mirror of
https://gitee.com/chinabugotech/hutool.git
synced 2025-08-18 20:38:02 +08:00
Compare commits
91 Commits
87a6bdeaaa
...
v5-master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f6ba182f52 | ||
|
|
cd40a65195 | ||
|
|
c738c2b42b | ||
|
|
0c19f0b9a4 | ||
|
|
a504fa860c | ||
|
|
a60c70ca86 | ||
|
|
2493b8da8b | ||
|
|
a10181dab2 | ||
|
|
28b21d7617 | ||
|
|
8deed41367 | ||
|
|
3b759bfae1 | ||
|
|
0c9c6ce655 | ||
|
|
5919837386 | ||
|
|
46249c257f | ||
|
|
a368ab544d | ||
|
|
6a58bfe9b2 | ||
|
|
41141cd824 | ||
|
|
2a602d0bd4 | ||
|
|
6c8fc623f0 | ||
|
|
484cdb9e4f | ||
|
|
62be1b0fe0 | ||
|
|
d015404248 | ||
|
|
d786c8e62d | ||
|
|
e1d68e61df | ||
|
|
2c8070f324 | ||
|
|
abae998de5 | ||
|
|
86e5f19197 | ||
|
|
fe597605cf | ||
|
|
5931264706 | ||
|
|
37fb3afce7 | ||
|
|
4cf41c444f | ||
|
|
7739f8b015 | ||
|
|
6d851488de | ||
|
|
82399a185e | ||
|
|
3be0111884 | ||
|
|
017a7b5c98 | ||
|
|
04f9784d08 | ||
|
|
008b9fd662 | ||
|
|
322c079abf | ||
|
|
5b10fedfef | ||
|
|
420e54a37d | ||
|
|
7f3be3038a | ||
|
|
a34637f071 | ||
|
|
dc54b89b87 | ||
|
|
4e1b468096 | ||
|
|
5f5ead1129 | ||
|
|
95ee9a0cb5 | ||
|
|
8f0777eadd | ||
|
|
cb0645dff9 | ||
|
|
c99616fa56 | ||
|
|
0426686dcb | ||
|
|
d8c9f1a06f | ||
|
|
2f3be3ebdb | ||
|
|
ce232cdd4f | ||
|
|
2b35ae8d09 | ||
|
|
f498459961 | ||
|
|
cc543bcc5d | ||
|
|
6cd936078b | ||
|
|
e65e006b24 | ||
|
|
1f2dc4fd3a | ||
|
|
7a2ef283ff | ||
|
|
d1988d4db9 | ||
|
|
2d7a64f660 | ||
|
|
8580250ce8 | ||
|
|
5ee00acaad | ||
|
|
e7d5a3b00f | ||
|
|
d1314d5905 | ||
|
|
a49523ffab | ||
|
|
f0c7b4d8f0 | ||
|
|
0ceed43e2e | ||
|
|
00cd1ac1cd | ||
|
|
0ed9c08507 | ||
|
|
abbe514479 | ||
|
|
87b69cf076 | ||
|
|
13d0edc957 | ||
|
|
12e29629cd | ||
|
|
45226d78ad | ||
|
|
6f346fc9a1 | ||
|
|
de5e7cc35d | ||
|
|
fc4a71200a | ||
|
|
25327585b1 | ||
|
|
f19f2f39e3 | ||
|
|
a76bfc8338 | ||
|
|
ed180873a5 | ||
|
|
3c49d9524a | ||
|
|
b8e6b1ecc0 | ||
|
|
2b777c3841 | ||
|
|
e458bca2a0 | ||
|
|
6e2e137b91 | ||
|
|
81159f75bb | ||
|
|
1e9c92c015 |
33
CHANGELOG.md
33
CHANGELOG.md
@@ -2,7 +2,33 @@
|
||||
# 🚀Changelog
|
||||
|
||||
-------------------------------------------------------------------------------------------------------------
|
||||
# 5.8.38(2025-04-26)
|
||||
# 5.8.39(2025-06-20)
|
||||
|
||||
### 🐣新特性
|
||||
* 【ai 】 增加SSE流式返回函数参数callback,增加超时时间配置,豆包、grok新增文生图接口,豆包生成视频支持使用model,新增HutoolAI平台
|
||||
* 【core 】 DesensitizedUtil新增护照号码脱敏功能(pr#1347@Gitee)
|
||||
* 【core 】 优化XXXToMapCopier的部分性能(pr#1345@Gitee)
|
||||
* 【http 】 `HttpConfig`增加参数`setIgnoreContentLength`可选忽略读取响应contentLength头(issue#ICB1B8@Gitee)
|
||||
* 【core 】 `Assert`新增断言给定集合为空的方法以及单元测试用例(pr#3952@Github)
|
||||
* 【db 】 Db添加FetchSize的全局设置(pr#3978@Github)
|
||||
* 【core 】 增加可召回批处理线程池执行器`RecyclableBatchThreadPoolExecutor`(pr#1343@Gitee)
|
||||
*
|
||||
### 🐞Bug修复
|
||||
* 【core 】 修复`NumberUtil`isNumber方法以L结尾没有小数点判断问题(issue#3938@Github)
|
||||
* 【core 】 修复`CharsequenceUtil`toLowerCase方法拼写错误(issue#3941@Github)
|
||||
* 【core 】 修复`UUID`equals的问题,改为final类(issue#3948@Github)
|
||||
* 【core 】 修复`Money`中金额分配的问题bug(issue#IC9Y35@Gitee)
|
||||
* 【poi 】 修复`ExcelPicUtil`中可能的空指针异常
|
||||
* 【core 】 修复`LunarFestival`中重复节日问题(issue#ICC8X3@Gitee)
|
||||
* 【core 】 修复`ThreadUtil`中中断异常处理丢失中断信息的问题,解决ConcurrencyTester资源未释放的问题(pr#1358@Gitee)
|
||||
* 【core 】 修复`TEL_400_800`正则规则太窄问题(issue#3967@Github)
|
||||
* 【core 】 修复`ClassUti`isNormalClass判断未排除String问题(issue#3965@Github)
|
||||
* 【core 】 修复`ZipUtil`中zlib和unZlib调用后资源未释放问题(issue#3976@Github)
|
||||
* 【core 】 修复`Money`类的setAmount方法没有获取当前币种的小数位数而是使用的默认小数位和在遇到非2小数位的币种(如日元使用 0 位)会导致金额设置错误问题(pr#3970@Github)
|
||||
* 【cahce 】 修复`AbstractCache`putWithoutLock方法可能导致的外部资源泄露问题(pr#3958@Github)
|
||||
|
||||
-------------------------------------------------------------------------------------------------------------
|
||||
# 5.8.38(2025-05-13)
|
||||
|
||||
### 🐣新特性
|
||||
* 【core 】 `PathUtil#del`增加null检查(pr#1331@Gitee)
|
||||
@@ -14,9 +40,14 @@
|
||||
* 【extra 】 `TemplateConfig`增加`setUseCache`方法(issue#IC3JRY@Gitee)
|
||||
* 【extra 】 `AbstractFtp`增加`rename`方法(issue#IC3PMI@Gitee)
|
||||
* 【core 】 优化`PropDesc`缓存注解判断,提升性能(pr#1335@Gitee)
|
||||
* 【core 】 添加`RecordUtil`支持record类(issue#3931@Github)
|
||||
* 【core 】 `Dict`的customKey方法访问权限修改为protected(pr#1340@Gitee)
|
||||
* 【ai 】 增加hutool-ai模块,对AI大模型的封装实现(pr#3937@Github)
|
||||
|
||||
### 🐞Bug修复
|
||||
* 【setting】 修复`Setting`autoLoad可能的加载为空的问题(issue#3919@Github)
|
||||
* 【db 】 修复某些数据库的getParameterMetaData会返回NULL,导致空指针的问题。(pr#3936@Github)
|
||||
* 【extra 】 修正`SshjSftp`在SftpSubsystem服务时报错问题(pr#1338@Gitee)
|
||||
|
||||
-------------------------------------------------------------------------------------------------------------
|
||||
# 5.8.37(2025-03-31)
|
||||
|
||||
11
README-EN.md
11
README-EN.md
@@ -86,8 +86,8 @@ Hutool exists to reduce code search costs and avoid bugs caused by imperfect cod
|
||||
## 🛠️Module
|
||||
A Java-based tool class for files, streams, encryption and decryption, transcoding, regular, thread, XML and other JDK methods for encapsulation,composing various Util tool classes, as well as providing the following modules:
|
||||
|
||||
| module | description |
|
||||
| -------------------|-------------------------------------------------------------------------------------------------------------------------|
|
||||
| module | description |
|
||||
|--------------------|-------------------------------------------------------------------------------------------------------------------------|
|
||||
| hutool-aop | JDK dynamic proxy encapsulation to provide non-IOC faceting support |
|
||||
| hutool-bloomFilter | Bloom filtering to provide some Hash algorithm Bloom filtering |
|
||||
| hutool-cache | Simple cache |
|
||||
@@ -107,6 +107,7 @@ A Java-based tool class for files, streams, encryption and decryption, transcodi
|
||||
| hutool-poi | Tools for working with Excel and Word in POI |
|
||||
| hutool-socket | Java-based tool classes for NIO and AIO sockets |
|
||||
| hutool-jwt | JSON Web Token (JWT) implement |
|
||||
| hutool-ai | AI implement |
|
||||
|
||||
Each module can be introduced individually, or all modules can be introduced by introducing `hutool-all` as required.
|
||||
|
||||
@@ -133,18 +134,18 @@ Each module can be introduced individually, or all modules can be introduced by
|
||||
<dependency>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-all</artifactId>
|
||||
<version>5.8.38</version>
|
||||
<version>5.8.39</version>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
### 🍐Gradle
|
||||
```
|
||||
implementation 'cn.hutool:hutool-all:5.8.38'
|
||||
implementation 'cn.hutool:hutool-all:5.8.39'
|
||||
```
|
||||
|
||||
## 📥Download
|
||||
|
||||
- [Maven Repo](https://repo1.maven.org/maven2/cn/hutool/hutool-all/5.8.38/)
|
||||
- [Maven Repo](https://repo1.maven.org/maven2/cn/hutool/hutool-all/5.8.39/)
|
||||
|
||||
> 🔔️note:
|
||||
> Hutool 5.x supports JDK8+ and is not tested on Android platforms, and cannot guarantee that all tool classes or tool methods are available.
|
||||
|
||||
11
README.md
11
README.md
@@ -74,8 +74,8 @@ Hutool = Hu + tool,是原公司项目底层代码剥离后的开源库,“Hu
|
||||
## 🛠️包含组件
|
||||
一个Java基础工具类,对文件、流、加密解密、转码、正则、线程、XML等JDK方法进行封装,组成各种Util工具类,同时提供以下组件:
|
||||
|
||||
| 模块 | 介绍 |
|
||||
| -------------------|---------------------------------------------------------------------------------- |
|
||||
| 模块 | 介绍 |
|
||||
|--------------------|---------------------------------------------------------------------------------- |
|
||||
| hutool-aop | JDK动态代理封装,提供非IOC下的切面支持 |
|
||||
| hutool-bloomFilter | 布隆过滤,提供一些Hash算法的布隆过滤 |
|
||||
| hutool-cache | 简单缓存实现 |
|
||||
@@ -95,6 +95,7 @@ Hutool = Hu + tool,是原公司项目底层代码剥离后的开源库,“Hu
|
||||
| hutool-poi | 针对POI中Excel和Word的封装 |
|
||||
| hutool-socket | 基于Java的NIO和AIO的Socket封装 |
|
||||
| hutool-jwt | JSON Web Token (JWT)封装实现 |
|
||||
| hutool-ai | AI大模型封装实现 |
|
||||
|
||||
可以根据需求对每个模块单独引入,也可以通过引入`hutool-all`方式引入所有模块。
|
||||
|
||||
@@ -123,20 +124,20 @@ Hutool = Hu + tool,是原公司项目底层代码剥离后的开源库,“Hu
|
||||
<dependency>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-all</artifactId>
|
||||
<version>5.8.38</version>
|
||||
<version>5.8.39</version>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
### 🍐Gradle
|
||||
```
|
||||
implementation 'cn.hutool:hutool-all:5.8.38'
|
||||
implementation 'cn.hutool:hutool-all:5.8.39'
|
||||
```
|
||||
|
||||
### 📥下载jar
|
||||
|
||||
点击以下链接,下载`hutool-all-X.X.X.jar`即可:
|
||||
|
||||
- [Maven中央库](https://repo1.maven.org/maven2/cn/hutool/hutool-all/5.8.38/)
|
||||
- [Maven中央库](https://repo1.maven.org/maven2/cn/hutool/hutool-all/5.8.39/)
|
||||
|
||||
> 🔔️注意
|
||||
> Hutool 5.x支持JDK8+,对Android平台没有测试,不能保证所有工具类或工具方法可用。
|
||||
|
||||
@@ -1 +1 @@
|
||||
5.8.38
|
||||
5.8.39
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
var _hmt = _hmt || [];
|
||||
(function () {
|
||||
var hm = document.createElement("script");
|
||||
hm.src = "https://hm.baidu.com/hm.js?f2c884fc06fca522c4105429259b8a73";
|
||||
hm.src = "https://hm.baidu.com/hm.js?a76bc7a2d60207f04195af1c51cdb9ba";
|
||||
var s = document.getElementsByTagName("script")[0];
|
||||
s.parentNode.insertBefore(hm, s);
|
||||
})();
|
||||
|
||||
@@ -1 +1 @@
|
||||
var version = '5.8.38'
|
||||
var version = '5.8.39'
|
||||
45
hutool-ai/pom.xml
Normal file
45
hutool-ai/pom.xml
Normal file
@@ -0,0 +1,45 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-parent</artifactId>
|
||||
<version>5.8.39</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>hutool-ai</artifactId>
|
||||
<name>${project.artifactId}</name>
|
||||
<description>Hutool AI大模型封装</description>
|
||||
|
||||
<properties>
|
||||
<Automatic-Module-Name>cn.hutool.ai</Automatic-Module-Name>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-core</artifactId>
|
||||
<version>${project.parent.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-http</artifactId>
|
||||
<version>${project.parent.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-log</artifactId>
|
||||
<version>${project.parent.version}</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-json</artifactId>
|
||||
<version>${project.parent.version}</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
87
hutool-ai/src/main/java/cn/hutool/ai/AIException.java
Normal file
87
hutool-ai/src/main/java/cn/hutool/ai/AIException.java
Normal file
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Hutool Team and hutool.cn
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package cn.hutool.ai;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
|
||||
/**
|
||||
* 异常处理类
|
||||
*/
|
||||
public class AIException extends RuntimeException {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 构造
|
||||
*
|
||||
* @param e 异常
|
||||
*/
|
||||
public AIException(final Throwable e) {
|
||||
super(e);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造
|
||||
*
|
||||
* @param message 消息
|
||||
*/
|
||||
public AIException(final String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造
|
||||
*
|
||||
* @param messageTemplate 消息模板
|
||||
* @param params 参数
|
||||
*/
|
||||
public AIException(String messageTemplate, Object... params) {
|
||||
super(StrUtil.format(messageTemplate, params));
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造
|
||||
*
|
||||
* @param message 消息
|
||||
* @param cause 被包装的子异常
|
||||
*/
|
||||
public AIException(final String message, final Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造
|
||||
*
|
||||
* @param message 消息
|
||||
* @param cause 被包装的子异常
|
||||
* @param enableSuppression 是否启用抑制
|
||||
* @param writableStackTrace 堆栈跟踪是否应该是可写的
|
||||
*/
|
||||
public AIException(final String message, final Throwable cause, final boolean enableSuppression, final boolean writableStackTrace) {
|
||||
super(message, cause, enableSuppression, writableStackTrace);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造
|
||||
*
|
||||
* @param throwable 被包装的子异常
|
||||
* @param messageTemplate 消息模板
|
||||
* @param params 参数
|
||||
*/
|
||||
public AIException(Throwable throwable, String messageTemplate, Object... params) {
|
||||
super(StrUtil.format(messageTemplate, params), throwable);
|
||||
}
|
||||
}
|
||||
80
hutool-ai/src/main/java/cn/hutool/ai/AIServiceFactory.java
Normal file
80
hutool-ai/src/main/java/cn/hutool/ai/AIServiceFactory.java
Normal file
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Hutool Team and hutool.cn
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package cn.hutool.ai;
|
||||
|
||||
import cn.hutool.ai.core.AIConfig;
|
||||
import cn.hutool.ai.core.AIService;
|
||||
import cn.hutool.ai.core.AIServiceProvider;
|
||||
import cn.hutool.core.util.ServiceLoaderUtil;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.ServiceLoader;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* 创建AIModelService的工厂类
|
||||
*
|
||||
* @author elichow
|
||||
* @since 5.8.38
|
||||
*/
|
||||
public class AIServiceFactory {
|
||||
|
||||
private static final Map<String, AIServiceProvider> providers = new ConcurrentHashMap<>();
|
||||
|
||||
// 加载所有 AIModelProvider 实现类
|
||||
static {
|
||||
final ServiceLoader<AIServiceProvider> loader = ServiceLoaderUtil.load(AIServiceProvider.class);
|
||||
for (final AIServiceProvider provider : loader) {
|
||||
providers.put(provider.getServiceName().toLowerCase(), provider);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取AI服务
|
||||
*
|
||||
* @param config AIConfig配置
|
||||
* @return AI服务实例
|
||||
* @since 5.8.38
|
||||
*/
|
||||
public static AIService getAIService(final AIConfig config) {
|
||||
return getAIService(config, AIService.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取AI服务
|
||||
*
|
||||
* @param config AIConfig配置
|
||||
* @param clazz AI服务类
|
||||
* @return clazz对应的AI服务类实例
|
||||
* @since 5.8.38
|
||||
* @param <T> AI服务类
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public static <T extends AIService> T getAIService(final AIConfig config, final Class<T> clazz) {
|
||||
final AIServiceProvider provider = providers.get(config.getModelName().toLowerCase());
|
||||
if (provider == null) {
|
||||
throw new IllegalArgumentException("Unsupported model: " + config.getModelName());
|
||||
}
|
||||
|
||||
final AIService service = provider.create(config);
|
||||
if (!clazz.isInstance(service)) {
|
||||
throw new AIException("Model service is not of type: " + clazz.getSimpleName());
|
||||
}
|
||||
|
||||
return (T) service;
|
||||
}
|
||||
}
|
||||
141
hutool-ai/src/main/java/cn/hutool/ai/AIUtil.java
Normal file
141
hutool-ai/src/main/java/cn/hutool/ai/AIUtil.java
Normal file
@@ -0,0 +1,141 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Hutool Team and hutool.cn
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package cn.hutool.ai;
|
||||
|
||||
import cn.hutool.ai.core.AIConfig;
|
||||
import cn.hutool.ai.core.AIService;
|
||||
import cn.hutool.ai.core.Message;
|
||||
import cn.hutool.ai.model.deepseek.DeepSeekService;
|
||||
import cn.hutool.ai.model.doubao.DoubaoService;
|
||||
import cn.hutool.ai.model.grok.GrokService;
|
||||
import cn.hutool.ai.model.hutool.HutoolService;
|
||||
import cn.hutool.ai.model.openai.OpenaiService;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* AI工具类
|
||||
*
|
||||
* @author elichow
|
||||
* @since 5.8.38
|
||||
*/
|
||||
public class AIUtil {
|
||||
|
||||
/**
|
||||
* 获取AI模型服务,每个大模型提供的功能会不一样,可以调用此方法指定不同AI服务类,调用不同的功能
|
||||
*
|
||||
* @param config 创建的AI服务模型的配置
|
||||
* @param clazz AI模型服务类
|
||||
* @return AIModelService的实现类实例
|
||||
* @since 5.8.38
|
||||
* @param <T> AIService实现类
|
||||
*/
|
||||
public static <T extends AIService> T getAIService(final AIConfig config, final Class<T> clazz) {
|
||||
return AIServiceFactory.getAIService(config, clazz);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取AI模型服务
|
||||
*
|
||||
* @param config 创建的AI服务模型的配置
|
||||
* @return AIModelService 其中只有公共方法
|
||||
* @since 5.8.38
|
||||
*/
|
||||
public static AIService getAIService(final AIConfig config) {
|
||||
return getAIService(config, AIService.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取Hutool-AI服务
|
||||
*
|
||||
* @param config 创建的AI服务模型的配置
|
||||
* @return HutoolService
|
||||
* @since 5.8.39
|
||||
*/
|
||||
public static HutoolService getHutoolService(final AIConfig config) {
|
||||
return getAIService(config, HutoolService.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取DeepSeek模型服务
|
||||
*
|
||||
* @param config 创建的AI服务模型的配置
|
||||
* @return DeepSeekService
|
||||
* @since 5.8.38
|
||||
*/
|
||||
public static DeepSeekService getDeepSeekService(final AIConfig config) {
|
||||
return getAIService(config, DeepSeekService.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取Doubao模型服务
|
||||
*
|
||||
* @param config 创建的AI服务模型的配置
|
||||
* @return DoubaoService
|
||||
* @since 5.8.38
|
||||
*/
|
||||
public static DoubaoService getDoubaoService(final AIConfig config) {
|
||||
return getAIService(config, DoubaoService.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取Grok模型服务
|
||||
*
|
||||
* @param config 创建的AI服务模型的配置
|
||||
* @return GrokService
|
||||
* @since 5.8.38
|
||||
*/
|
||||
public static GrokService getGrokService(final AIConfig config) {
|
||||
return getAIService(config, GrokService.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取Openai模型服务
|
||||
*
|
||||
* @param config 创建的AI服务模型的配置
|
||||
* @return OpenAIService
|
||||
* @since 5.8.38
|
||||
*/
|
||||
public static OpenaiService getOpenAIService(final AIConfig config) {
|
||||
return getAIService(config, OpenaiService.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* AI大模型对话功能
|
||||
*
|
||||
* @param config 创建的AI服务模型的配置
|
||||
* @param prompt 需要对话的内容
|
||||
* @return AI模型返回的Response响应字符串
|
||||
* @since 5.8.38
|
||||
*/
|
||||
public static String chat(final AIConfig config, final String prompt) {
|
||||
return getAIService(config).chat(prompt);
|
||||
}
|
||||
|
||||
/**
|
||||
* AI大模型对话功能
|
||||
*
|
||||
* @param config 创建的AI服务模型的配置
|
||||
* @param messages 由目前为止的对话组成的消息列表,可以设置role,content。详细参考官方文档
|
||||
* @return AI模型返回的Response响应字符串
|
||||
* @since 5.8.38
|
||||
*/
|
||||
public static String chat(final AIConfig config, final List<Message> messages) {
|
||||
return getAIService(config).chat(messages);
|
||||
}
|
||||
|
||||
}
|
||||
62
hutool-ai/src/main/java/cn/hutool/ai/ModelName.java
Normal file
62
hutool-ai/src/main/java/cn/hutool/ai/ModelName.java
Normal file
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Hutool Team and hutool.cn
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package cn.hutool.ai;
|
||||
|
||||
/**
|
||||
* 模型厂商的名称(不指具体的模型)
|
||||
*
|
||||
* @author elichow
|
||||
* @since 5.8.38
|
||||
*/
|
||||
public enum ModelName {
|
||||
|
||||
/**
|
||||
* hutool
|
||||
*/
|
||||
HUTOOL("hutool"),
|
||||
/**
|
||||
* deepSeek
|
||||
*/
|
||||
DEEPSEEK("deepSeek"),
|
||||
/**
|
||||
* openai
|
||||
*/
|
||||
OPENAI("openai"),
|
||||
/**
|
||||
* doubao
|
||||
*/
|
||||
DOUBAO("doubao"),
|
||||
/**
|
||||
* grok
|
||||
*/
|
||||
GROK("grok");
|
||||
|
||||
private final String value;
|
||||
|
||||
ModelName(final String value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取值
|
||||
*
|
||||
* @return 值
|
||||
*/
|
||||
public String getValue() {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
195
hutool-ai/src/main/java/cn/hutool/ai/Models.java
Normal file
195
hutool-ai/src/main/java/cn/hutool/ai/Models.java
Normal file
@@ -0,0 +1,195 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Hutool Team and hutool.cn
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package cn.hutool.ai;
|
||||
|
||||
/**
|
||||
* 各模型厂商包含的model(指具体的模型)
|
||||
*
|
||||
* @author elichow
|
||||
* @since 5.8.38
|
||||
*/
|
||||
public class Models {
|
||||
|
||||
|
||||
// Hutool的模型
|
||||
public enum Hutool {
|
||||
HUTOOL("hutool");
|
||||
|
||||
private final String model;
|
||||
|
||||
Hutool(String model) {
|
||||
this.model = model;
|
||||
}
|
||||
|
||||
public String getModel() {
|
||||
return model;
|
||||
}
|
||||
}
|
||||
|
||||
// DeepSeek的模型
|
||||
public enum DeepSeek {
|
||||
DEEPSEEK_CHAT("deepseek-chat"),
|
||||
DEEPSEEK_REASONER("deepseek-reasoner");
|
||||
|
||||
private final String model;
|
||||
|
||||
DeepSeek(String model) {
|
||||
this.model = model;
|
||||
}
|
||||
|
||||
public String getModel() {
|
||||
return model;
|
||||
}
|
||||
}
|
||||
|
||||
// Openai的模型
|
||||
public enum Openai {
|
||||
GPT_4_5_PREVIEW("gpt-4.5-preview"),
|
||||
GPT_4O("gpt-4o"),
|
||||
CHATGPT_4O_LATEST("chatgpt-4o-latest"),
|
||||
GPT_4O_MINI("gpt-4o-mini"),
|
||||
O1("o1"),
|
||||
O1_MINI("o1-mini"),
|
||||
O1_PREVIEW("o1-preview"),
|
||||
O3_MINI("o3-mini"),
|
||||
GPT_4O_REALTIME_PREVIEW("gpt-4o-realtime-preview"),
|
||||
GPT_4O_MINI_REALTIME_PREVIEW("gpt-4o-mini-realtime-preview"),
|
||||
GPT_4O_AUDIO_PREVIEW("gpt-4o-audio-preview"),
|
||||
GPT_4O_MINI_AUDIO_PREVIEW("gpt-4o-mini-audio-preview"),
|
||||
GPT_4_TURBO("gpt-4-turbo"),
|
||||
GPT_4_TURBO_PREVIEW("gpt-4-turbo-preview"),
|
||||
GPT_4("gpt-4"),
|
||||
GPT_3_5_TURBO_0125("gpt-3.5-turbo-0125"),
|
||||
GPT_3_5_TURBO("gpt-3.5-turbo"),
|
||||
GPT_3_5_TURBO_1106("gpt-3.5-turbo-1106"),
|
||||
GPT_3_5_TURBO_INSTRUCT("gpt-3.5-turbo-instruct"),
|
||||
DALL_E_3("dall-e-3"),
|
||||
DALL_E_2("dall-e-2"),
|
||||
TTS_1("tts-1"),
|
||||
TTS_1_HD("tts-1-hd"),
|
||||
WHISPER_1("whisper-1"),
|
||||
TEXT_EMBEDDING_3_LARGE("text-embedding-3-large"),
|
||||
TEXT_EMBEDDING_3_SMALL("text-embedding-3-small"),
|
||||
TEXT_EMBEDDING_ADA_002("text-embedding-ada-002"),
|
||||
OMNI_MODERATION_LATEST("omni-moderation-latest"),
|
||||
OMNI_MODERATION_2024_09_26("omni-moderation-2024-09-26"),
|
||||
TEXT_MODERATION_LATEST("text-moderation-latest"),
|
||||
TEXT_MODERATION_STABLE("text-moderation-stable"),
|
||||
TEXT_MODERATION_007("text-moderation-007"),
|
||||
BABBAGE_002("babbage-002"),
|
||||
DAVINCI_002("davinci-002");
|
||||
|
||||
private final String model;
|
||||
|
||||
Openai(String model) {
|
||||
this.model = model;
|
||||
}
|
||||
|
||||
public String getModel() {
|
||||
return model;
|
||||
}
|
||||
}
|
||||
|
||||
// Doubao的模型
|
||||
public enum Doubao {
|
||||
DOUBAO_1_5_PRO_32K("doubao-1.5-pro-32k-250115"),
|
||||
DOUBAO_1_5_PRO_256K("doubao-1.5-pro-256k-250115"),
|
||||
DOUBAO_1_5_LITE_32K("doubao-1.5-lite-32k-250115"),
|
||||
DEEPSEEK_R1("deepseek-r1-250120"),
|
||||
DEEPSEEK_R1_DISTILL_QWEN_32B("deepseek-r1-distill-qwen-32b-250120"),
|
||||
DEEPSEEK_R1_DISTILL_QWEN_7B("deepseek-r1-distill-qwen-7b-250120"),
|
||||
DEEPSEEK_V3("deepseek-v3-241226"),
|
||||
DOUBAO_PRO_4K_240515("doubao-pro-4k-240515"),
|
||||
DOUBAO_PRO_4K_CHARACTER_240728("doubao-pro-4k-character-240728"),
|
||||
DOUBAO_PRO_4K_FUNCTIONCALL_240615("doubao-pro-4k-functioncall-240615"),
|
||||
DOUBAO_PRO_4K_BROWSING_240524("doubao-pro-4k-browsing-240524"),
|
||||
DOUBAO_PRO_32K_241215("doubao-pro-32k-241215"),
|
||||
DOUBAO_PRO_32K_FUNCTIONCALL_241028("doubao-pro-32k-functioncall-241028"),
|
||||
DOUBAO_PRO_32K_BROWSING_241115("doubao-pro-32k-browsing-241115"),
|
||||
DOUBAO_PRO_32K_CHARACTER_241215("doubao-pro-32k-character-241215"),
|
||||
DOUBAO_PRO_128K_240628("doubao-pro-128k-240628"),
|
||||
DOUBAO_PRO_256K_240828("doubao-pro-256k-240828"),
|
||||
DOUBAO_LITE_4K_240328("doubao-lite-4k-240328"),
|
||||
DOUBAO_LITE_4K_PRETRAIN_CHARACTER_240516("doubao-lite-4k-pretrain-character-240516"),
|
||||
DOUBAO_LITE_32K_240828("doubao-lite-32k-240828"),
|
||||
DOUBAO_LITE_32K_CHARACTER_241015("doubao-lite-32k-character-241015"),
|
||||
DOUBAO_LITE_128K_240828("240828"),
|
||||
MOONSHOT_V1_8K("moonshot-v1-8k"),
|
||||
MOONSHOT_V1_32K("moonshot-v1-32k"),
|
||||
MOONSHOT_V1_128K("moonshot-v1-128k"),
|
||||
CHATGLM3_130B_FC("chatglm3-130b-fc-v1.0"),
|
||||
CHATGLM3_130_FIN("chatglm3-130-fin-v1.0-update"),
|
||||
MISTRAL_7B("mistral-7b-instruct-v0.2"),
|
||||
DOUBAO_1_5_VISION_PRO_32K("doubao-1.5-vision-pro-32k-250115"),
|
||||
DOUBAO_VISION_PRO_32K("doubao-vision-pro-32k-241008"),
|
||||
DOUBAO_VISION_LITE_32K("doubao-vision-lite-32k-241015"),
|
||||
DOUBAO_EMBEDDING_LARGE("doubao-embedding-large-text-240915"),
|
||||
DOUBAO_EMBEDDING_TEXT_240715("doubao-embedding-text-240715"),
|
||||
DOUBAO_EMBEDDING_VISION("doubao-embedding-vision-241215"),
|
||||
DOUBAO_SEEDREAM_3_0_T2I("doubao-seedream-3-0-t2i-250415"),
|
||||
Doubao_Seedance_1_0_lite_t2v("doubao-seedance-1-0-lite-t2v-250428"),
|
||||
Doubao_Seedance_1_0_lite_i2v("doubao-seedance-1-0-lite-i2v-250428"),
|
||||
Wan2_1_14B_t2v("wan2-1-14b-t2v-250225"),
|
||||
Wan2_1_14B_i2v("wan2-1-14b-i2v-250225");
|
||||
|
||||
private final String model;
|
||||
|
||||
Doubao(String model) {
|
||||
this.model = model;
|
||||
}
|
||||
|
||||
public String getModel() {
|
||||
return model;
|
||||
}
|
||||
}
|
||||
|
||||
// Grok的模型
|
||||
public enum Grok {
|
||||
GROK_3_BETA_LATEST("grok-3-beta"),
|
||||
GROK_3_BETA("grok-3-beta"),
|
||||
GROK_3("grok-3-beta"),
|
||||
GROK_3_MINI_FAST_LATEST("grok-3-mini-fast-beta"),
|
||||
GROK_3_MINI_FAST_BETA("grok-3-mini-fast-beta"),
|
||||
GROK_3_MINI_FAST("grok-3-mini-fast-beta"),
|
||||
GROK_3_FAST_LATEST("grok-3-fast-beta"),
|
||||
GROK_3_FAST_BETA("grok-3-fast-beta"),
|
||||
GROK_3_FAST("grok-3-fast-beta"),
|
||||
GROK_3_MINI_LATEST("grok-3-mini-beta"),
|
||||
GROK_3_MINI_BETA("grok-3-mini-beta"),
|
||||
GROK_3_MINI("grok-3-mini-beta"),
|
||||
GROK_2_IMAGE_LATEST("grok-2-image-1212"),
|
||||
GROK_2_IMAGE("grok-2-image-1212"),
|
||||
GROK_2_IMAGE_1212("grok-2-image-1212"),
|
||||
grok_2_latest("grok-2-1212"),
|
||||
GROK_2("grok-2-1212"),
|
||||
GROK_2_1212("grok-2-1212"),
|
||||
GROK_2_VISION_1212("grok-2-vision-1212"),
|
||||
GROK_BETA("grok-beta"),
|
||||
GROK_VISION_BETA("grok-vision-beta");
|
||||
|
||||
private final String model;
|
||||
|
||||
Grok(String model) {
|
||||
this.model = model;
|
||||
}
|
||||
|
||||
public String getModel() {
|
||||
return model;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
145
hutool-ai/src/main/java/cn/hutool/ai/core/AIConfig.java
Normal file
145
hutool-ai/src/main/java/cn/hutool/ai/core/AIConfig.java
Normal file
@@ -0,0 +1,145 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Hutool Team and hutool.cn
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package cn.hutool.ai.core;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* AI配置类
|
||||
*
|
||||
* @author elichow
|
||||
* @since 5.8.38
|
||||
*/
|
||||
public interface AIConfig {
|
||||
|
||||
/**
|
||||
* 获取模型(厂商)名称
|
||||
*
|
||||
* @return 模型(厂商)名称
|
||||
* @since 5.8.38
|
||||
*/
|
||||
default String getModelName() {
|
||||
return this.getClass().getSimpleName();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置apiKey
|
||||
*
|
||||
* @param apiKey apiKey
|
||||
* @since 5.8.38
|
||||
*/
|
||||
void setApiKey(String apiKey);
|
||||
|
||||
/**
|
||||
* 获取apiKey
|
||||
*
|
||||
* @return apiKey
|
||||
* @since 5.8.38
|
||||
*/
|
||||
String getApiKey();
|
||||
|
||||
/**
|
||||
* 设置apiUrl
|
||||
*
|
||||
* @param apiUrl api请求地址
|
||||
* @since 5.8.38
|
||||
*/
|
||||
void setApiUrl(String apiUrl);
|
||||
|
||||
/**
|
||||
* 获取apiUrl
|
||||
*
|
||||
* @return apiUrl
|
||||
* @since 5.8.38
|
||||
*/
|
||||
String getApiUrl();
|
||||
|
||||
/**
|
||||
* 设置model
|
||||
*
|
||||
* @param model model
|
||||
* @since 5.8.38
|
||||
*/
|
||||
void setModel(String model);
|
||||
|
||||
/**
|
||||
* 返回model
|
||||
*
|
||||
* @return model
|
||||
* @since 5.8.38
|
||||
*/
|
||||
String getModel();
|
||||
|
||||
/**
|
||||
* 设置动态参数
|
||||
*
|
||||
* @param key 参数字段
|
||||
* @param value 参数值
|
||||
* @since 5.8.38
|
||||
*/
|
||||
void putAdditionalConfigByKey(String key, Object value);
|
||||
|
||||
/**
|
||||
* 获取动态参数
|
||||
*
|
||||
* @param key 参数字段
|
||||
* @return 参数值
|
||||
* @since 5.8.38
|
||||
*/
|
||||
Object getAdditionalConfigByKey(String key);
|
||||
|
||||
/**
|
||||
* 获取动态参数列表
|
||||
*
|
||||
* @return 参数列表Map
|
||||
* @since 5.8.38
|
||||
*/
|
||||
Map<String, Object> getAdditionalConfigMap();
|
||||
|
||||
/**
|
||||
* 设置连接超时时间
|
||||
*
|
||||
* @param timeout 连接超时时间
|
||||
* @since 5.8.39
|
||||
*/
|
||||
void setTimeout(int timeout);
|
||||
|
||||
/**
|
||||
* 获取连接超时时间
|
||||
*
|
||||
* @return timeout
|
||||
* @since 5.8.39
|
||||
*/
|
||||
int getTimeout();
|
||||
|
||||
/**
|
||||
* 设置读取超时时间
|
||||
*
|
||||
* @param readTimeout 连接超时时间
|
||||
* @since 5.8.39
|
||||
*/
|
||||
void setReadTimeout(int readTimeout);
|
||||
|
||||
/**
|
||||
* 获取读取超时时间
|
||||
*
|
||||
* @return readTimeout
|
||||
* @since 5.8.39
|
||||
*/
|
||||
int getReadTimeout();
|
||||
|
||||
}
|
||||
146
hutool-ai/src/main/java/cn/hutool/ai/core/AIConfigBuilder.java
Normal file
146
hutool-ai/src/main/java/cn/hutool/ai/core/AIConfigBuilder.java
Normal file
@@ -0,0 +1,146 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Hutool Team and hutool.cn
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package cn.hutool.ai.core;
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
|
||||
/**
|
||||
* 用于AIConfig的创建,创建同时支持链式设置参数
|
||||
*
|
||||
* @author elichow
|
||||
* @since 5.8.38
|
||||
*/
|
||||
public class AIConfigBuilder {
|
||||
|
||||
private final AIConfig config;
|
||||
|
||||
/**
|
||||
* 构造
|
||||
*
|
||||
* @param modelName 模型厂商的名称(注意不是指具体的模型)
|
||||
*/
|
||||
public AIConfigBuilder(final String modelName) {
|
||||
try {
|
||||
// 获取配置类
|
||||
final Class<? extends AIConfig> configClass = AIConfigRegistry.getConfigClass(modelName);
|
||||
if (configClass == null) {
|
||||
throw new IllegalArgumentException("Unsupported model: " + modelName);
|
||||
}
|
||||
|
||||
// 使用反射创建实例
|
||||
final Constructor<? extends AIConfig> constructor = configClass.getDeclaredConstructor();
|
||||
config = constructor.newInstance();
|
||||
} catch (final Exception e) {
|
||||
throw new RuntimeException("Failed to create AIConfig instance", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置apiKey
|
||||
*
|
||||
* @param apiKey apiKey
|
||||
* @return config
|
||||
* @since 5.8.38
|
||||
*/
|
||||
public synchronized AIConfigBuilder setApiKey(final String apiKey) {
|
||||
if (apiKey != null) {
|
||||
config.setApiKey(apiKey);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置AI模型请求API接口的地址,不设置为默认值
|
||||
*
|
||||
* @param apiUrl API接口地址
|
||||
* @return config
|
||||
* @since 5.8.38
|
||||
*/
|
||||
public synchronized AIConfigBuilder setApiUrl(final String apiUrl) {
|
||||
if (apiUrl != null) {
|
||||
config.setApiUrl(apiUrl);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置具体的model,不设置为默认值
|
||||
*
|
||||
* @param model 具体model的名称
|
||||
* @return config
|
||||
* @since 5.8.38
|
||||
*/
|
||||
public synchronized AIConfigBuilder setModel(final String model) {
|
||||
if (model != null) {
|
||||
config.setModel(model);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 动态设置Request请求体中的属性字段,每个模型功能支持的字段请参照对应的官方文档
|
||||
*
|
||||
* @param key Request中的支持的属性名
|
||||
* @param value 设置的属性值
|
||||
* @return config
|
||||
* @since 5.8.38
|
||||
*/
|
||||
public AIConfigBuilder putAdditionalConfig(final String key, final Object value) {
|
||||
if (value != null) {
|
||||
config.putAdditionalConfigByKey(key, value);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置连接超时时间,不设置为默认值
|
||||
*
|
||||
* @param timeout 超时时间
|
||||
* @return config
|
||||
* @since 5.8.39
|
||||
*/
|
||||
public synchronized AIConfigBuilder setTimout(final int timeout) {
|
||||
if (timeout > 0) {
|
||||
config.setTimeout(timeout);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置读取超时时间,不设置为默认值
|
||||
*
|
||||
* @param readTimout 取超时时间
|
||||
* @return config
|
||||
* @since 5.8.39
|
||||
*/
|
||||
public synchronized AIConfigBuilder setReadTimout(final int readTimout) {
|
||||
if (readTimout > 0) {
|
||||
config.setReadTimeout(readTimout);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回config实例
|
||||
*
|
||||
* @return config
|
||||
* @since 5.8.38
|
||||
*/
|
||||
public AIConfig build() {
|
||||
return config;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Hutool Team and hutool.cn
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package cn.hutool.ai.core;
|
||||
|
||||
import cn.hutool.core.util.ServiceLoaderUtil;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.ServiceLoader;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* AIConfig实现类的加载器
|
||||
*
|
||||
* @author elichow
|
||||
* @since 5.8.38
|
||||
*/
|
||||
public class AIConfigRegistry {
|
||||
|
||||
private static final Map<String, Class<? extends AIConfig>> configClasses = new ConcurrentHashMap<>();
|
||||
|
||||
// 加载所有 AIConfig 实现类
|
||||
static {
|
||||
final ServiceLoader<AIConfig> loader = ServiceLoaderUtil.load(AIConfig.class);
|
||||
for (final AIConfig config : loader) {
|
||||
configClasses.put(config.getModelName().toLowerCase(), config.getClass());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据模型名称获取AIConfig实现类
|
||||
*
|
||||
* @param modelName 模型名称
|
||||
* @return AIConfig实现类
|
||||
*/
|
||||
public static Class<? extends AIConfig> getConfigClass(final String modelName) {
|
||||
return configClasses.get(modelName.toLowerCase());
|
||||
}
|
||||
}
|
||||
75
hutool-ai/src/main/java/cn/hutool/ai/core/AIService.java
Normal file
75
hutool-ai/src/main/java/cn/hutool/ai/core/AIService.java
Normal file
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Hutool Team and hutool.cn
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package cn.hutool.ai.core;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* 模型公共的API功能,特有的功能在model.xx.XXService下定义
|
||||
*
|
||||
* @author elichow
|
||||
* @since 5.8.38
|
||||
*/
|
||||
public interface AIService {
|
||||
|
||||
/**
|
||||
* 对话
|
||||
*
|
||||
* @param prompt user题词
|
||||
* @return AI回答
|
||||
* @since 5.8.38
|
||||
*/
|
||||
default String chat(String prompt){
|
||||
final List<Message> messages = new ArrayList<>();
|
||||
messages.add(new Message("system", "You are a helpful assistant"));
|
||||
messages.add(new Message("user", prompt));
|
||||
return chat(messages);
|
||||
}
|
||||
|
||||
/**
|
||||
* 对话-SSE流式输出
|
||||
* @param prompt user题词
|
||||
* @param callback 流式数据回调函数
|
||||
* @since 5.8.39
|
||||
*/
|
||||
default void chat(String prompt, final Consumer<String> callback){
|
||||
final List<Message> messages = new ArrayList<>();
|
||||
messages.add(new Message("system", "You are a helpful assistant"));
|
||||
messages.add(new Message("user", prompt));
|
||||
chat(messages, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* 对话
|
||||
*
|
||||
* @param messages 由目前为止的对话组成的消息列表,可以设置role,content。详细参考官方文档
|
||||
* @return AI回答
|
||||
* @since 5.8.38
|
||||
*/
|
||||
String chat(final List<Message> messages);
|
||||
|
||||
|
||||
/**
|
||||
* 对话-SSE流式输出
|
||||
* @param messages 由目前为止的对话组成的消息列表,可以设置role,content。详细参考官方文档
|
||||
* @param callback 流式数据回调函数
|
||||
* @since 5.8.39
|
||||
*/
|
||||
void chat(final List<Message> messages, final Consumer<String> callback);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Hutool Team and hutool.cn
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package cn.hutool.ai.core;
|
||||
|
||||
/**
|
||||
* 用于加载AI服务,每一个通过SPI创建的AI服务都要实现此接口
|
||||
*
|
||||
* @author elichow
|
||||
* @since 5.8.38
|
||||
*/
|
||||
public interface AIServiceProvider {
|
||||
|
||||
/**
|
||||
* 获取AI服务名称
|
||||
*
|
||||
* @return AI服务名称
|
||||
* @since 5.8.38
|
||||
*/
|
||||
String getServiceName();
|
||||
|
||||
/**
|
||||
* 创建AI服务实例
|
||||
*
|
||||
* @param config AIConfig配置
|
||||
* @param <T> AIService实现类
|
||||
* @return AI服务实例
|
||||
* @since 5.8.38
|
||||
*/
|
||||
<T extends AIService> T create(final AIConfig config);
|
||||
}
|
||||
158
hutool-ai/src/main/java/cn/hutool/ai/core/BaseAIService.java
Normal file
158
hutool-ai/src/main/java/cn/hutool/ai/core/BaseAIService.java
Normal file
@@ -0,0 +1,158 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Hutool Team and hutool.cn
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package cn.hutool.ai.core;
|
||||
|
||||
import cn.hutool.ai.AIException;
|
||||
import cn.hutool.http.*;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* 基础AIService,包含基公共参数和公共方法
|
||||
*
|
||||
* @author elichow
|
||||
* @since 5.8.38
|
||||
*/
|
||||
public class BaseAIService {
|
||||
|
||||
protected final AIConfig config;
|
||||
|
||||
/**
|
||||
* 构造方法
|
||||
*
|
||||
* @param config AI配置
|
||||
*/
|
||||
public BaseAIService(final AIConfig config) {
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送Get请求
|
||||
* @param endpoint 请求节点
|
||||
* @return 请求响应
|
||||
*/
|
||||
protected HttpResponse sendGet(final String endpoint) {
|
||||
//链式构建请求
|
||||
try {
|
||||
//设置超时3分钟
|
||||
return HttpRequest.get(config.getApiUrl() + endpoint)
|
||||
.header(Header.ACCEPT, "application/json")
|
||||
.header(Header.AUTHORIZATION, "Bearer " + config.getApiKey())
|
||||
.timeout(config.getTimeout())
|
||||
.execute();
|
||||
} catch (final AIException e) {
|
||||
throw new AIException("Failed to send GET request: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送Post请求
|
||||
* @param endpoint 请求节点
|
||||
* @param paramJson 请求参数json
|
||||
* @return 请求响应
|
||||
*/
|
||||
protected HttpResponse sendPost(final String endpoint, final String paramJson) {
|
||||
//链式构建请求
|
||||
try {
|
||||
return HttpRequest.post(config.getApiUrl() + endpoint)
|
||||
.header(Header.CONTENT_TYPE, "application/json")
|
||||
.header(Header.ACCEPT, "application/json")
|
||||
.header(Header.AUTHORIZATION, "Bearer " + config.getApiKey())
|
||||
.body(paramJson)
|
||||
.timeout(config.getTimeout())
|
||||
.execute();
|
||||
} catch (final AIException e) {
|
||||
throw new AIException("Failed to send POST request:" + e.getMessage(), e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送表单请求
|
||||
* @param endpoint 请求节点
|
||||
* @param paramMap 请求参数map
|
||||
* @return 请求响应
|
||||
*/
|
||||
protected HttpResponse sendFormData(final String endpoint, final Map<String, Object> paramMap) {
|
||||
//链式构建请求
|
||||
try {
|
||||
//设置超时3分钟
|
||||
return HttpRequest.post(config.getApiUrl() + endpoint)
|
||||
.header(Header.CONTENT_TYPE, "multipart/form-data")
|
||||
.header(Header.ACCEPT, "application/json")
|
||||
.header(Header.AUTHORIZATION, "Bearer " + config.getApiKey())
|
||||
.form(paramMap)
|
||||
.timeout(config.getTimeout())
|
||||
.execute();
|
||||
} catch (final AIException e) {
|
||||
throw new AIException("Failed to send POST request:" + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 支持流式返回的 POST 请求
|
||||
*
|
||||
* @param endpoint 请求地址
|
||||
* @param paramMap 请求参数
|
||||
* @param callback 流式数据回调函数
|
||||
*/
|
||||
protected void sendPostStream(final String endpoint, final Map<String, Object> paramMap, Consumer<String> callback) {
|
||||
HttpURLConnection connection = null;
|
||||
try {
|
||||
// 创建连接
|
||||
URL apiUrl = new URL(config.getApiUrl() + endpoint);
|
||||
connection = (HttpURLConnection) apiUrl.openConnection();
|
||||
connection.setRequestMethod(Method.POST.name());
|
||||
connection.setRequestProperty(Header.CONTENT_TYPE.getValue(), "application/json");
|
||||
connection.setRequestProperty(Header.AUTHORIZATION.getValue(), "Bearer " + config.getApiKey());
|
||||
connection.setDoOutput(true);
|
||||
//5分钟
|
||||
connection.setReadTimeout(config.getReadTimeout());
|
||||
//3分钟
|
||||
connection.setConnectTimeout(config.getTimeout());
|
||||
// 发送请求体
|
||||
try (OutputStream os = connection.getOutputStream()) {
|
||||
String jsonInputString = JSONUtil.toJsonStr(paramMap);
|
||||
os.write(jsonInputString.getBytes());
|
||||
os.flush();
|
||||
}
|
||||
|
||||
// 读取流式响应
|
||||
try (BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()))) {
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
// 调用回调函数处理每一行数据
|
||||
callback.accept(line);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
callback.accept("{\"error\": \"" + e.getMessage() + "\"}");
|
||||
} finally {
|
||||
// 关闭连接
|
||||
if (connection != null) {
|
||||
connection.disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
107
hutool-ai/src/main/java/cn/hutool/ai/core/BaseConfig.java
Normal file
107
hutool-ai/src/main/java/cn/hutool/ai/core/BaseConfig.java
Normal file
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Hutool Team and hutool.cn
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package cn.hutool.ai.core;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* Config基础类,定义模型配置的基本属性
|
||||
*
|
||||
* @author elichow
|
||||
* @since 5.8.38
|
||||
*/
|
||||
public class BaseConfig implements AIConfig {
|
||||
|
||||
//apiKey
|
||||
protected volatile String apiKey;
|
||||
//API请求地址
|
||||
protected volatile String apiUrl;
|
||||
//具体模型
|
||||
protected volatile String model;
|
||||
//动态扩展字段
|
||||
protected Map<String, Object> additionalConfig = new ConcurrentHashMap<>();
|
||||
//连接超时时间
|
||||
protected volatile int timeout = 180000;
|
||||
//读取超时时间
|
||||
protected volatile int readTimeout = 300000;
|
||||
|
||||
@Override
|
||||
public void setApiKey(final String apiKey) {
|
||||
this.apiKey = apiKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getApiKey() {
|
||||
return apiKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setApiUrl(final String apiUrl) {
|
||||
this.apiUrl = apiUrl;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getApiUrl() {
|
||||
return apiUrl;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setModel(final String model) {
|
||||
this.model = model;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getModel() {
|
||||
return model;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void putAdditionalConfigByKey(final String key, final Object value) {
|
||||
this.additionalConfig.put(key, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getAdditionalConfigByKey(final String key) {
|
||||
return additionalConfig.get(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getAdditionalConfigMap() {
|
||||
return new ConcurrentHashMap<>(additionalConfig);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getTimeout() {
|
||||
return timeout;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTimeout(final int timeout) {
|
||||
this.timeout = timeout;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getReadTimeout() {
|
||||
return readTimeout;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setReadTimeout(final int readTimeout) {
|
||||
this.readTimeout = readTimeout;
|
||||
}
|
||||
}
|
||||
59
hutool-ai/src/main/java/cn/hutool/ai/core/Message.java
Normal file
59
hutool-ai/src/main/java/cn/hutool/ai/core/Message.java
Normal file
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Hutool Team and hutool.cn
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package cn.hutool.ai.core;
|
||||
|
||||
/**
|
||||
* 公共Message类
|
||||
*
|
||||
* @author elichow
|
||||
* @since 5.8.38
|
||||
*/
|
||||
public class Message {
|
||||
//角色 注意:如果设置系统消息,请放在messages列表的第一位
|
||||
private final String role;
|
||||
//内容
|
||||
private final Object content;
|
||||
|
||||
/**
|
||||
* 构造
|
||||
*
|
||||
* @param role 角色
|
||||
* @param content 内容
|
||||
*/
|
||||
public Message(final String role, final Object content) {
|
||||
this.role = role;
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取角色
|
||||
*
|
||||
* @return 角色
|
||||
*/
|
||||
public String getRole() {
|
||||
return role;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取内容
|
||||
*
|
||||
* @return 内容
|
||||
*/
|
||||
public Object getContent() {
|
||||
return content;
|
||||
}
|
||||
}
|
||||
24
hutool-ai/src/main/java/cn/hutool/ai/core/package-info.java
Normal file
24
hutool-ai/src/main/java/cn/hutool/ai/core/package-info.java
Normal file
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Hutool Team and hutool.cn
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* AI相关基础类
|
||||
*
|
||||
* @author elichow
|
||||
* @since 5.8.38
|
||||
*/
|
||||
|
||||
package cn.hutool.ai.core;
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Hutool Team and hutool.cn
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package cn.hutool.ai.model.deepseek;
|
||||
|
||||
/**
|
||||
* deepSeek公共类
|
||||
*
|
||||
* @author elichow
|
||||
* @since 5.8.38
|
||||
*/
|
||||
public class DeepSeekCommon {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Hutool Team and hutool.cn
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package cn.hutool.ai.model.deepseek;
|
||||
|
||||
import cn.hutool.ai.Models;
|
||||
import cn.hutool.ai.core.BaseConfig;
|
||||
|
||||
/**
|
||||
* DeepSeek配置类,初始化API接口地址,设置默认的模型
|
||||
*
|
||||
* @author elichow
|
||||
* @since 5.8.38
|
||||
*/
|
||||
public class DeepSeekConfig extends BaseConfig {
|
||||
|
||||
private final String API_URL = "https://api.deepseek.com";
|
||||
|
||||
private final String DEFAULT_MODEL = Models.DeepSeek.DEEPSEEK_CHAT.getModel();
|
||||
|
||||
public DeepSeekConfig() {
|
||||
setApiUrl(API_URL);
|
||||
setModel(DEFAULT_MODEL);
|
||||
}
|
||||
|
||||
public DeepSeekConfig(String apiKey) {
|
||||
this();
|
||||
setApiKey(apiKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getModelName() {
|
||||
return "deepSeek";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Hutool Team and hutool.cn
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package cn.hutool.ai.model.deepseek;
|
||||
|
||||
import cn.hutool.ai.core.AIConfig;
|
||||
import cn.hutool.ai.core.AIServiceProvider;
|
||||
|
||||
/**
|
||||
* 创建DeepSeek服务实现类
|
||||
*
|
||||
* @author elichow
|
||||
* @since 5.8.38
|
||||
*/
|
||||
public class DeepSeekProvider implements AIServiceProvider {
|
||||
|
||||
@Override
|
||||
public String getServiceName() {
|
||||
return "deepSeek";
|
||||
}
|
||||
|
||||
@Override
|
||||
public DeepSeekService create(final AIConfig config) {
|
||||
return new DeepSeekServiceImpl(config);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Hutool Team and hutool.cn
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package cn.hutool.ai.model.deepseek;
|
||||
|
||||
import cn.hutool.ai.core.AIService;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* deepSeek支持的扩展接口
|
||||
*
|
||||
* @author elichow
|
||||
* @since 5.8.38
|
||||
*/
|
||||
public interface DeepSeekService extends AIService {
|
||||
|
||||
/**
|
||||
* 模型beta功能
|
||||
*
|
||||
* @param prompt 题词
|
||||
* @return AI的回答
|
||||
* @since 5.8.38
|
||||
*/
|
||||
String beta(String prompt);
|
||||
|
||||
/**
|
||||
* 模型beta功能-SSE流式输出
|
||||
* @param prompt 题词
|
||||
* @param callback 流式数据回调函数
|
||||
* @since 5.8.39
|
||||
*/
|
||||
void beta(String prompt, final Consumer<String> callback);
|
||||
|
||||
/**
|
||||
* 列出所有模型列表
|
||||
*
|
||||
* @return model列表
|
||||
* @since 5.8.38
|
||||
*/
|
||||
String models();
|
||||
|
||||
/**
|
||||
* 查询余额
|
||||
*
|
||||
* @return 余额
|
||||
* @since 5.8.38
|
||||
*/
|
||||
String balance();
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Hutool Team and hutool.cn
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package cn.hutool.ai.model.deepseek;
|
||||
|
||||
import cn.hutool.ai.core.AIConfig;
|
||||
import cn.hutool.ai.core.BaseAIService;
|
||||
import cn.hutool.ai.core.Message;
|
||||
import cn.hutool.core.thread.ThreadUtil;
|
||||
import cn.hutool.http.HttpResponse;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* DeepSeek服务,AI具体功能的实现
|
||||
*
|
||||
* @author elichow
|
||||
* @since 5.8.38
|
||||
*/
|
||||
public class DeepSeekServiceImpl extends BaseAIService implements DeepSeekService {
|
||||
|
||||
//对话补全
|
||||
private final String CHAT_ENDPOINT = "/chat/completions";
|
||||
//FIM补全(beta)
|
||||
private final String BETA_ENDPOINT = "/beta/completions";
|
||||
//列出模型
|
||||
private final String MODELS_ENDPOINT = "/models";
|
||||
//余额查询
|
||||
private final String BALANCE_ENDPOINT = "/user/balance";
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*
|
||||
* @param config AI配置
|
||||
*/
|
||||
public DeepSeekServiceImpl(final AIConfig config) {
|
||||
//初始化DeepSeek客户端
|
||||
super(config);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String chat(final List<Message> messages) {
|
||||
final String paramJson = buildChatRequestBody(messages);
|
||||
final HttpResponse response = sendPost(CHAT_ENDPOINT, paramJson);
|
||||
return response.body();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void chat(final List<Message> messages, final Consumer<String> callback) {
|
||||
Map<String, Object> paramMap = buildChatStreamRequestBody(messages);
|
||||
ThreadUtil.newThread(() -> sendPostStream(CHAT_ENDPOINT, paramMap, callback::accept), "deepseek-chat-sse").start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String beta(final String prompt) {
|
||||
final String paramJson = buildBetaRequestBody(prompt);
|
||||
final HttpResponse response = sendPost(BETA_ENDPOINT, paramJson);
|
||||
return response.body();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beta(final String prompt, final Consumer<String> callback) {
|
||||
Map<String, Object> paramMap = buildBetaStreamRequestBody(prompt);
|
||||
ThreadUtil.newThread(() -> sendPostStream(BETA_ENDPOINT, paramMap, callback::accept), "deepseek-beta-sse").start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String models() {
|
||||
final HttpResponse response = sendGet(MODELS_ENDPOINT);
|
||||
return response.body();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String balance() {
|
||||
final HttpResponse response = sendGet(BALANCE_ENDPOINT);
|
||||
return response.body();
|
||||
}
|
||||
|
||||
// 构建chat请求体
|
||||
private String buildChatRequestBody(final List<Message> messages) {
|
||||
//使用JSON工具
|
||||
final Map<String, Object> paramMap = new HashMap<>();
|
||||
paramMap.put("model", config.getModel());
|
||||
paramMap.put("messages", messages);
|
||||
//合并其他参数
|
||||
paramMap.putAll(config.getAdditionalConfigMap());
|
||||
|
||||
return JSONUtil.toJsonStr(paramMap);
|
||||
}
|
||||
|
||||
// 构建chatStream请求体
|
||||
private Map<String, Object> buildChatStreamRequestBody(final List<Message> messages) {
|
||||
//使用JSON工具
|
||||
final Map<String, Object> paramMap = new HashMap<>();
|
||||
paramMap.put("stream", true);
|
||||
paramMap.put("model", config.getModel());
|
||||
paramMap.put("messages", messages);
|
||||
//合并其他参数
|
||||
paramMap.putAll(config.getAdditionalConfigMap());
|
||||
|
||||
return paramMap;
|
||||
}
|
||||
|
||||
// 构建beta请求体
|
||||
private String buildBetaRequestBody(final String prompt) {
|
||||
// 定义消息结构
|
||||
//使用JSON工具
|
||||
final Map<String, Object> paramMap = new HashMap<>();
|
||||
paramMap.put("model", config.getModel());
|
||||
paramMap.put("prompt", prompt);
|
||||
//合并其他参数
|
||||
paramMap.putAll(config.getAdditionalConfigMap());
|
||||
|
||||
return JSONUtil.toJsonStr(paramMap);
|
||||
}
|
||||
|
||||
// 构建betaStream请求体
|
||||
private Map<String, Object> buildBetaStreamRequestBody(final String prompt) {
|
||||
//使用JSON工具
|
||||
final Map<String, Object> paramMap = new HashMap<>();
|
||||
paramMap.put("stream", true);
|
||||
paramMap.put("model", config.getModel());
|
||||
paramMap.put("prompt", prompt);
|
||||
//合并其他参数
|
||||
paramMap.putAll(config.getAdditionalConfigMap());
|
||||
|
||||
return paramMap;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Hutool Team and hutool.cn
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* 对deepSeek的封装实现
|
||||
*
|
||||
* @author elichow
|
||||
* @since 5.8.38
|
||||
*/
|
||||
|
||||
package cn.hutool.ai.model.deepseek;
|
||||
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Hutool Team and hutool.cn
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package cn.hutool.ai.model.doubao;
|
||||
|
||||
/**
|
||||
* doubao公共类
|
||||
*
|
||||
* @author elichow
|
||||
* @since 5.8.38
|
||||
*/
|
||||
public class DoubaoCommon {
|
||||
|
||||
//doubao上下文缓存参数
|
||||
public enum DoubaoContext {
|
||||
|
||||
SESSION("session"),
|
||||
COMMON_PREFIX("common_prefix");
|
||||
|
||||
private final String mode;
|
||||
|
||||
DoubaoContext(String mode) {
|
||||
this.mode = mode;
|
||||
}
|
||||
|
||||
public String getMode() {
|
||||
return mode;
|
||||
}
|
||||
}
|
||||
|
||||
//doubao视觉参数
|
||||
public enum DoubaoVision {
|
||||
|
||||
AUTO("auto"),
|
||||
LOW("low"),
|
||||
HIGH("high");
|
||||
|
||||
private final String detail;
|
||||
|
||||
DoubaoVision(String detail) {
|
||||
this.detail = detail;
|
||||
}
|
||||
|
||||
public String getDetail() {
|
||||
return detail;
|
||||
}
|
||||
}
|
||||
|
||||
//doubao视频生成参数
|
||||
public enum DoubaoVideo {
|
||||
|
||||
//宽高比例
|
||||
RATIO_16_9("--rt", "16:9"),//[1280, 720]
|
||||
RATIO_4_3("--rt", "4:3"),//[960, 720]
|
||||
RATIO_1_1("--rt", "1:1"),//[720, 720]
|
||||
RATIO_3_4("--rt", "3:4"),//[720, 960]
|
||||
RATIO_9_16("--rt", "9:16"),//[720, 1280]
|
||||
RATIO_21_9("--rt", "21:9"),//[1280, 544]
|
||||
|
||||
//生成视频时长
|
||||
DURATION_5("--dur", 5),//文生视频,图生视频
|
||||
DURATION_10("--dur", 10),//文生视频
|
||||
|
||||
//帧率,即一秒时间内视频画面数量
|
||||
FPS_5("--fps", 24),
|
||||
|
||||
//视频分辨率
|
||||
RESOLUTION_5("--rs", "720p"),
|
||||
|
||||
//生成视频是否包含水印
|
||||
WATERMARK_TRUE("--wm", true),
|
||||
WATERMARK_FALSE("--wm", false);
|
||||
|
||||
private final String type;
|
||||
private final Object value;
|
||||
|
||||
DoubaoVideo(String type, Object value) {
|
||||
this.type = type;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public Object getValue() {
|
||||
if (value instanceof String) {
|
||||
return (String) value;
|
||||
} else if (value instanceof Integer) {
|
||||
return (Integer) value;
|
||||
} else if (value instanceof Boolean) {
|
||||
return (Boolean) value;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Hutool Team and hutool.cn
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package cn.hutool.ai.model.doubao;
|
||||
|
||||
import cn.hutool.ai.Models;
|
||||
import cn.hutool.ai.core.BaseConfig;
|
||||
|
||||
/**
|
||||
* Doubao配置类,初始化API接口地址,设置默认的模型
|
||||
*
|
||||
* @author elichow
|
||||
* @since 5.8.38
|
||||
*/
|
||||
public class DoubaoConfig extends BaseConfig {
|
||||
|
||||
private final String API_URL = "https://ark.cn-beijing.volces.com/api/v3";
|
||||
|
||||
private final String DEFAULT_MODEL = Models.Doubao.DOUBAO_1_5_LITE_32K.getModel();
|
||||
|
||||
public DoubaoConfig() {
|
||||
setApiUrl(API_URL);
|
||||
setModel(DEFAULT_MODEL);
|
||||
}
|
||||
|
||||
public DoubaoConfig(String apiKey) {
|
||||
this();
|
||||
setApiKey(apiKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getModelName() {
|
||||
return "doubao";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Hutool Team and hutool.cn
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package cn.hutool.ai.model.doubao;
|
||||
|
||||
import cn.hutool.ai.core.AIConfig;
|
||||
import cn.hutool.ai.core.AIServiceProvider;
|
||||
|
||||
/**
|
||||
* 创建Doubap服务实现类
|
||||
*
|
||||
* @author elichow
|
||||
* @since 5.8.38
|
||||
*/
|
||||
public class DoubaoProvider implements AIServiceProvider {
|
||||
|
||||
@Override
|
||||
public String getServiceName() {
|
||||
return "doubao";
|
||||
}
|
||||
|
||||
@Override
|
||||
public DoubaoService create(final AIConfig config) {
|
||||
return new DoubaoServiceImpl(config);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Hutool Team and hutool.cn
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package cn.hutool.ai.model.doubao;
|
||||
|
||||
import cn.hutool.ai.core.AIService;
|
||||
import cn.hutool.ai.core.Message;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* doubao支持的扩展接口
|
||||
*
|
||||
* @author elichow
|
||||
* @since 5.8.38
|
||||
*/
|
||||
public interface DoubaoService extends AIService {
|
||||
|
||||
/**
|
||||
* 图像理解:模型会依据传入的图片信息以及问题,给出回复。
|
||||
*
|
||||
* @param prompt 提问
|
||||
* @param images 传入的图片列表地址/或者图片Base64编码图片列表(URI形式)
|
||||
* @return AI回答
|
||||
* @since 5.8.38
|
||||
*/
|
||||
default String chatVision(String prompt, final List<String> images) {
|
||||
return chatVision(prompt, images, DoubaoCommon.DoubaoVision.AUTO.getDetail());
|
||||
}
|
||||
|
||||
/**
|
||||
* 图像理解-SSE流式输出
|
||||
*
|
||||
* @param prompt 提问
|
||||
* @param images 图片列表/或者图片Base64编码图片列表(URI形式)
|
||||
* @param callback 流式数据回调函数
|
||||
* @since 5.8.39
|
||||
*/
|
||||
default void chatVision(String prompt, final List<String> images, final Consumer<String> callback) {
|
||||
chatVision(prompt, images, DoubaoCommon.DoubaoVision.AUTO.getDetail(), callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* 图像理解:模型会依据传入的图片信息以及问题,给出回复。
|
||||
*
|
||||
* @param prompt 提问
|
||||
* @param images 图片列表/或者图片Base64编码图片列表(URI形式)
|
||||
* @param detail 手动设置图片的质量,取值范围high、low、auto,默认为auto
|
||||
* @return AI回答
|
||||
* @since 5.8.38
|
||||
*/
|
||||
String chatVision(String prompt, final List<String> images, String detail);
|
||||
|
||||
/**
|
||||
* 图像理解-SSE流式输出
|
||||
*
|
||||
* @param prompt 提问
|
||||
* @param images 传入的图片列表地址/或者图片Base64编码图片列表(URI形式)
|
||||
* @param detail 手动设置图片的质量,取值范围high、low、auto,默认为auto
|
||||
* @param callback 流式数据回调函数
|
||||
* @since 5.8.39
|
||||
*/
|
||||
void chatVision(String prompt, final List<String> images, String detail, final Consumer<String> callback);
|
||||
|
||||
/**
|
||||
* 创建视频生成任务
|
||||
* 注意:调用该方法时,配置config中的model为您创建的推理接入点(Endpoint)ID。详细参考官方文档
|
||||
*
|
||||
* @param text 文本提示词
|
||||
* @param image 图片/或者图片Base64编码图片(URI形式)
|
||||
* @param videoParams 视频参数列表
|
||||
* @return 生成任务id
|
||||
* @since 5.8.38
|
||||
*/
|
||||
String videoTasks(String text, String image, final List<DoubaoCommon.DoubaoVideo> videoParams);
|
||||
|
||||
/**
|
||||
* 创建视频生成任务
|
||||
* 注意:调用该方法时,配置config中的model为生成视频的模型或者您创建的推理接入点(Endpoint)ID。详细参考官方文档
|
||||
*
|
||||
* @param text 文本提示词
|
||||
* @param image 图片/或者图片Base64编码图片(URI形式)
|
||||
* @return 生成任务id
|
||||
* @since 5.8.38
|
||||
*/
|
||||
default String videoTasks(String text, String image) {
|
||||
return videoTasks(text, image, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询视频生成任务信息
|
||||
*
|
||||
* @param taskId 通过创建生成视频任务返回的生成任务id
|
||||
* @return 生成任务信息
|
||||
* @since 5.8.38
|
||||
*/
|
||||
String getVideoTasksInfo(String taskId);
|
||||
|
||||
/**
|
||||
* 文本向量化
|
||||
*
|
||||
* @param input 需要向量化的内容列表,支持中文、英文
|
||||
* @return 处理后的向量信息
|
||||
* @since 5.8.38
|
||||
*/
|
||||
String embeddingText(String[] input);
|
||||
|
||||
/**
|
||||
* 图文向量化:仅支持单一文本、单张图片或文本与图片的组合输入(即一段文本 + 一张图片),暂不支持批量文本 / 图片的同时处理
|
||||
*
|
||||
* @param text 需要向量化的内容
|
||||
* @param image 需要向量化的图片地址/或者图片Base64编码图片(URI形式)
|
||||
* @return 处理后的向量信息
|
||||
* @since 5.8.38
|
||||
*/
|
||||
String embeddingVision(String text, String image);
|
||||
|
||||
/**
|
||||
* 应用(Bot) config中model设置为您创建的应用ID
|
||||
*
|
||||
* @param messages 由对话组成的消息列表。如系统人设,背景信息等,用户自定义的信息
|
||||
* @return AI回答
|
||||
* @since 5.8.38
|
||||
*/
|
||||
String botsChat(final List<Message> messages);
|
||||
|
||||
/**
|
||||
* 应用(Bot)-SSE流式输出 config中model设置为您创建的应用ID
|
||||
*
|
||||
* @param messages 由对话组成的消息列表。如系统人设,背景信息等,用户自定义的信息
|
||||
* @param callback 流式数据回调函数
|
||||
* @since 5.8.39
|
||||
*/
|
||||
void botsChat(final List<Message> messages, final Consumer<String> callback);
|
||||
|
||||
/**
|
||||
* 分词:可以将文本转换为模型可理解的 token id,并返回文本的 tokens 数量、token id、 token 在原始文本中的偏移量等信息
|
||||
*
|
||||
* @param text 需要分词的内容列表
|
||||
* @return 分词结果
|
||||
* @since 5.8.38
|
||||
*/
|
||||
String tokenization(String[] text);
|
||||
|
||||
/**
|
||||
* 批量推理 Chat
|
||||
* 注意:调用该方法时,配置config中的model为您创建的批量推理接入点(Endpoint)ID。详细参考官方文档
|
||||
* 该方法不支持流式
|
||||
*
|
||||
* @param prompt chat内容
|
||||
* @return AI回答
|
||||
* @since 5.8.38
|
||||
*/
|
||||
default String batchChat(String prompt){
|
||||
final List<Message> messages = new ArrayList<>();
|
||||
messages.add(new Message("system", "You are a helpful assistant"));
|
||||
messages.add(new Message("user", prompt));
|
||||
return batchChat(messages);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量推理 Chat
|
||||
* 注意:调用该方法时,配置config中的model为您创建的批量推理接入点(Endpoint)ID。详细参考官方文档
|
||||
* 该方法不支持流式
|
||||
*
|
||||
* @param messages 由对话组成的消息列表。如系统人设,背景信息等,用户自定义的信息
|
||||
* @return AI回答
|
||||
* @since 5.8.38
|
||||
*/
|
||||
String batchChat(final List<Message> messages);
|
||||
|
||||
/**
|
||||
* 创建上下文缓存: 创建上下文缓存,获得缓存 id字段后,在上下文缓存对话 API中使用。
|
||||
* 注意:调用该方法时,配置config中的model为您创建的推理接入点(Endpoint)ID,
|
||||
* 推理接入点中使用的模型需要在模型管理中开启缓存功能。详细参考官方文档
|
||||
*
|
||||
* @param messages 由对话组成的消息列表。如系统人设,背景信息等,用户自定义的信息
|
||||
* @param mode 上下文缓存的类型,详细参考官方文档 默认为session
|
||||
* @return 返回的缓存id
|
||||
* @since 5.8.38
|
||||
*/
|
||||
String createContext(final List<Message> messages, String mode);
|
||||
|
||||
/**
|
||||
* 创建上下文缓存: 创建上下文缓存,获得缓存 id字段后,在上下文缓存对话 API中使用。
|
||||
* 注意:调用该方法时,配置config中的model为您创建的推理接入点(Endpoint)ID,
|
||||
* 推理接入点中使用的模型需要在模型管理中开启缓存功能。详细参考官方文档
|
||||
*
|
||||
* @param messages 由对话组成的消息列表。如系统人设,背景信息等,用户自定义的信息
|
||||
* @return 返回的缓存id
|
||||
* @since 5.8.38
|
||||
*/
|
||||
default String createContext(final List<Message> messages) {
|
||||
return createContext(messages, DoubaoCommon.DoubaoContext.SESSION.getMode());
|
||||
}
|
||||
|
||||
/**
|
||||
* 上下文缓存对话: 向大模型发起带上下文缓存的请求
|
||||
* 注意:配置config中的model可以为您创建的推理接入点(Endpoint)ID,也可以是支持chat的model
|
||||
*
|
||||
* @param prompt 对话的内容题词
|
||||
* @param contextId 创建上下文缓存后获取的缓存id
|
||||
* @return AI的回答
|
||||
* @since 5.8.38
|
||||
*/
|
||||
default String chatContext(String prompt, String contextId){
|
||||
final List<Message> messages = new ArrayList<>();
|
||||
messages.add(new Message("user", prompt));
|
||||
return chatContext(messages, contextId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 上下文缓存对话-SSE流式输出
|
||||
* 注意:配置config中的model可以为您创建的推理接入点(Endpoint)ID,也可以是支持chat的model
|
||||
*
|
||||
* @param prompt 对话的内容题词
|
||||
* @param contextId 创建上下文缓存后获取的缓存id
|
||||
* @param callback 流式数据回调函数
|
||||
* @since 5.8.39
|
||||
*/
|
||||
default void chatContext(String prompt, String contextId, final Consumer<String> callback){
|
||||
final List<Message> messages = new ArrayList<>();
|
||||
messages.add(new Message("user", prompt));
|
||||
chatContext(messages, contextId, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* 上下文缓存对话: 向大模型发起带上下文缓存的请求
|
||||
* 注意:配置config中的model可以为您创建的推理接入点(Endpoint)ID,也可以是支持chat的model
|
||||
*
|
||||
* @param messages 对话的信息 不支持最后一个元素的role设置为assistant。如使用session 缓存(mode设置为session)传入最新一轮对话的信息,无需传入历史信息
|
||||
* @param contextId 创建上下文缓存后获取的缓存id
|
||||
* @return AI的回答
|
||||
* @since 5.8.38
|
||||
*/
|
||||
String chatContext(final List<Message> messages, String contextId);
|
||||
|
||||
/**
|
||||
* 上下文缓存对话-SSE流式输出
|
||||
* 注意:配置config中的model可以为您创建的推理接入点(Endpoint)ID,也可以是支持chat的model
|
||||
*
|
||||
* @param messages 对话的信息 不支持最后一个元素的role设置为assistant。如使用session 缓存(mode设置为session)传入最新一轮对话的信息,无需传入历史信息
|
||||
* @param contextId 创建上下文缓存后获取的缓存id
|
||||
* @param callback 流式数据回调函数
|
||||
* @since 5.8.39
|
||||
*/
|
||||
void chatContext(final List<Message> messages, String contextId, final Consumer<String> callback);
|
||||
|
||||
/**
|
||||
* 文生图
|
||||
* 请设置config中model为支持图片功能的模型,目前支持Doubao-Seedream-3.0-t2i
|
||||
*
|
||||
* @param prompt 题词
|
||||
* @return 包含生成图片的url
|
||||
* @since 5.8.39
|
||||
*/
|
||||
String imagesGenerations(String prompt);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,439 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Hutool Team and hutool.cn
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package cn.hutool.ai.model.doubao;
|
||||
|
||||
import cn.hutool.ai.core.AIConfig;
|
||||
import cn.hutool.ai.core.BaseAIService;
|
||||
import cn.hutool.ai.core.Message;
|
||||
import cn.hutool.core.thread.ThreadUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.http.HttpResponse;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* Doubao服务,AI具体功能的实现
|
||||
*
|
||||
* @author elichow
|
||||
* @since 5.8.38
|
||||
*/
|
||||
public class DoubaoServiceImpl extends BaseAIService implements DoubaoService {
|
||||
|
||||
//对话
|
||||
private final String CHAT_ENDPOINT = "/chat/completions";
|
||||
//文本向量化
|
||||
private final String EMBEDDING_TEXT = "/embeddings";
|
||||
//图文向量化
|
||||
private final String EMBEDDING_VISION = "/embeddings/multimodal";
|
||||
//应用bots
|
||||
private final String BOTS_CHAT = "/bots/chat/completions";
|
||||
//分词
|
||||
private final String TOKENIZATION = "/tokenization";
|
||||
//批量推理chat
|
||||
private final String BATCH_CHAT = "/batch/chat/completions";
|
||||
//创建上下文缓存
|
||||
private final String CREATE_CONTEXT = "/context/create";
|
||||
//上下文缓存对话
|
||||
private final String CHAT_CONTEXT = "/context/chat/completions";
|
||||
//创建视频生成任务
|
||||
private final String CREATE_VIDEO = "/contents/generations/tasks";
|
||||
//文生图
|
||||
private final String IMAGES_GENERATIONS = "/images/generations";
|
||||
|
||||
public DoubaoServiceImpl(final AIConfig config) {
|
||||
//初始化doubao客户端
|
||||
super(config);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String chat(final List<Message> messages) {
|
||||
String paramJson = buildChatRequestBody(messages);
|
||||
final HttpResponse response = sendPost(CHAT_ENDPOINT, paramJson);
|
||||
return response.body();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void chat(final List<Message> messages, final Consumer<String> callback) {
|
||||
Map<String, Object> paramMap = buildChatStreamRequestBody(messages);
|
||||
ThreadUtil.newThread(() -> sendPostStream(CHAT_ENDPOINT, paramMap, callback::accept), "doubao-chat-sse").start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String chatVision(String prompt, final List<String> images, String detail) {
|
||||
String paramJson = buildChatVisionRequestBody(prompt, images, detail);
|
||||
final HttpResponse response = sendPost(CHAT_ENDPOINT, paramJson);
|
||||
return response.body();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void chatVision(String prompt, List<String> images, String detail, Consumer<String> callback) {
|
||||
Map<String, Object> paramMap = buildChatVisionStreamRequestBody(prompt, images, detail);
|
||||
ThreadUtil.newThread(() -> sendPostStream(CHAT_ENDPOINT, paramMap, callback::accept), "doubao-chatVision-sse").start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String videoTasks(String text, String image, final List<DoubaoCommon.DoubaoVideo> videoParams) {
|
||||
String paramJson = buildGenerationsTasksRequestBody(text, image, videoParams);
|
||||
final HttpResponse response = sendPost(CREATE_VIDEO, paramJson);
|
||||
return response.body();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getVideoTasksInfo(String taskId) {
|
||||
final HttpResponse response = sendGet(CREATE_VIDEO + "/" + taskId);
|
||||
return response.body();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String embeddingText(String[] input) {
|
||||
String paramJson = buildEmbeddingTextRequestBody(input);
|
||||
final HttpResponse response = sendPost(EMBEDDING_TEXT, paramJson);
|
||||
return response.body();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String embeddingVision(String text, String image) {
|
||||
String paramJson = buildEmbeddingVisionRequestBody(text, image);
|
||||
final HttpResponse response = sendPost(EMBEDDING_VISION, paramJson);
|
||||
return response.body();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String botsChat(final List<Message> messages) {
|
||||
String paramJson = buildBotsChatRequestBody(messages);
|
||||
final HttpResponse response = sendPost(BOTS_CHAT, paramJson);
|
||||
return response.body();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void botsChat(List<Message> messages, Consumer<String> callback) {
|
||||
Map<String, Object> paramMap = buildBotsChatStreamRequestBody(messages);
|
||||
ThreadUtil.newThread(() -> sendPostStream(BOTS_CHAT, paramMap, callback::accept), "doubao-botsChat-sse").start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String tokenization(String[] text) {
|
||||
String paramJson = buildTokenizationRequestBody(text);
|
||||
final HttpResponse response = sendPost(TOKENIZATION, paramJson);
|
||||
return response.body();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String batchChat(final List<Message> messages) {
|
||||
String paramJson = buildBatchChatRequestBody(messages);
|
||||
final HttpResponse response = sendPost(BATCH_CHAT, paramJson);
|
||||
return response.body();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String createContext(final List<Message> messages, String mode) {
|
||||
String paramJson = buildCreateContextRequest(messages, mode);
|
||||
final HttpResponse response = sendPost(CREATE_CONTEXT, paramJson);
|
||||
return response.body();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String chatContext(final List<Message> messages, String contextId) {
|
||||
String paramJson = buildChatContentRequestBody(messages, contextId);
|
||||
final HttpResponse response = sendPost(CHAT_CONTEXT, paramJson);
|
||||
return response.body();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void chatContext(final List<Message> messages, String contextId, final Consumer<String> callback) {
|
||||
Map<String, Object> paramMap = buildChatContentStreamRequestBody(messages, contextId);
|
||||
ThreadUtil.newThread(() -> sendPostStream(CHAT_CONTEXT, paramMap, callback::accept), "doubao-chatContext-sse").start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String imagesGenerations(String prompt) {
|
||||
String paramJson = buildImagesGenerationsRequestBody(prompt);
|
||||
final HttpResponse response = sendPost(IMAGES_GENERATIONS, paramJson);
|
||||
return response.body();
|
||||
}
|
||||
|
||||
// 构建chat请求体
|
||||
private String buildChatRequestBody(final List<Message> messages) {
|
||||
//使用JSON工具
|
||||
final Map<String, Object> paramMap = new HashMap<>();
|
||||
paramMap.put("model", config.getModel());
|
||||
paramMap.put("messages", messages);
|
||||
//合并其他参数
|
||||
paramMap.putAll(config.getAdditionalConfigMap());
|
||||
|
||||
return JSONUtil.toJsonStr(paramMap);
|
||||
}
|
||||
|
||||
// 构建chatStream请求体
|
||||
private Map<String, Object> buildChatStreamRequestBody(final List<Message> messages) {
|
||||
//使用JSON工具
|
||||
final Map<String, Object> paramMap = new HashMap<>();
|
||||
paramMap.put("stream", true);
|
||||
paramMap.put("model", config.getModel());
|
||||
paramMap.put("messages", messages);
|
||||
//合并其他参数
|
||||
paramMap.putAll(config.getAdditionalConfigMap());
|
||||
|
||||
return paramMap;
|
||||
}
|
||||
|
||||
//构建chatVision请求体
|
||||
private String buildChatVisionRequestBody(String prompt, final List<String> images, String detail) {
|
||||
// 定义消息结构
|
||||
final List<Message> messages = new ArrayList<>();
|
||||
final List<Object> content = new ArrayList<>();
|
||||
|
||||
final Map<String, String> contentMap = new HashMap<>();
|
||||
contentMap.put("type", "text");
|
||||
contentMap.put("text", prompt);
|
||||
content.add(contentMap);
|
||||
for (String img : images) {
|
||||
HashMap<String, Object> imgUrlMap = new HashMap<>();
|
||||
imgUrlMap.put("type", "image_url");
|
||||
HashMap<String, String> urlMap = new HashMap<>();
|
||||
urlMap.put("url", img);
|
||||
urlMap.put("detail", detail);
|
||||
imgUrlMap.put("image_url", urlMap);
|
||||
content.add(imgUrlMap);
|
||||
}
|
||||
|
||||
messages.add(new Message("user", content));
|
||||
|
||||
//使用JSON工具
|
||||
final Map<String, Object> paramMap = new HashMap<>();
|
||||
paramMap.put("model", config.getModel());
|
||||
paramMap.put("messages", messages);
|
||||
//合并其他参数
|
||||
paramMap.putAll(config.getAdditionalConfigMap());
|
||||
return JSONUtil.toJsonStr(paramMap);
|
||||
}
|
||||
|
||||
private Map<String, Object> buildChatVisionStreamRequestBody(String prompt, final List<String> images, String detail) {
|
||||
// 定义消息结构
|
||||
final List<Message> messages = new ArrayList<>();
|
||||
final List<Object> content = new ArrayList<>();
|
||||
|
||||
final Map<String, String> contentMap = new HashMap<>();
|
||||
contentMap.put("type", "text");
|
||||
contentMap.put("text", prompt);
|
||||
content.add(contentMap);
|
||||
for (String img : images) {
|
||||
HashMap<String, Object> imgUrlMap = new HashMap<>();
|
||||
imgUrlMap.put("type", "image_url");
|
||||
HashMap<String, String> urlMap = new HashMap<>();
|
||||
urlMap.put("url", img);
|
||||
urlMap.put("detail", detail);
|
||||
imgUrlMap.put("image_url", urlMap);
|
||||
content.add(imgUrlMap);
|
||||
}
|
||||
|
||||
messages.add(new Message("user", content));
|
||||
|
||||
//使用JSON工具
|
||||
final Map<String, Object> paramMap = new HashMap<>();
|
||||
paramMap.put("stream", true);
|
||||
paramMap.put("model", config.getModel());
|
||||
paramMap.put("messages", messages);
|
||||
//合并其他参数
|
||||
paramMap.putAll(config.getAdditionalConfigMap());
|
||||
return paramMap;
|
||||
}
|
||||
|
||||
//构建文本向量化请求体
|
||||
private String buildEmbeddingTextRequestBody(String[] input) {
|
||||
//使用JSON工具
|
||||
final Map<String, Object> paramMap = new HashMap<>();
|
||||
paramMap.put("model", config.getModel());
|
||||
paramMap.put("input", input);
|
||||
//合并其他参数
|
||||
paramMap.putAll(config.getAdditionalConfigMap());
|
||||
return JSONUtil.toJsonStr(paramMap);
|
||||
}
|
||||
|
||||
//构建图文向量化请求体
|
||||
private String buildEmbeddingVisionRequestBody(String text, String image) {
|
||||
//使用JSON工具
|
||||
final Map<String, Object> paramMap = new HashMap<>();
|
||||
paramMap.put("model", config.getModel());
|
||||
|
||||
final List<Object> input = new ArrayList<>();
|
||||
//添加文本参数
|
||||
if (!StrUtil.isBlank(text)) {
|
||||
final Map<String, String> textMap = new HashMap<>();
|
||||
textMap.put("type", "text");
|
||||
textMap.put("text", text);
|
||||
input.add(textMap);
|
||||
}
|
||||
//添加图片参数
|
||||
if (!StrUtil.isBlank(image)) {
|
||||
final Map<String, Object> imgUrlMap = new HashMap<>();
|
||||
imgUrlMap.put("type", "image_url");
|
||||
final Map<String, String> urlMap = new HashMap<>();
|
||||
urlMap.put("url", image);
|
||||
imgUrlMap.put("image_url", urlMap);
|
||||
input.add(imgUrlMap);
|
||||
}
|
||||
|
||||
paramMap.put("input", input);
|
||||
//合并其他参数
|
||||
paramMap.putAll(config.getAdditionalConfigMap());
|
||||
|
||||
return JSONUtil.toJsonStr(paramMap);
|
||||
}
|
||||
|
||||
//构建应用chat请求体
|
||||
private String buildBotsChatRequestBody(final List<Message> messages) {
|
||||
return buildChatRequestBody(messages);
|
||||
}
|
||||
|
||||
private Map<String, Object> buildBotsChatStreamRequestBody(final List<Message> messages) {
|
||||
return buildChatStreamRequestBody(messages);
|
||||
}
|
||||
|
||||
//构建分词请求体
|
||||
private String buildTokenizationRequestBody(String[] text) {
|
||||
final Map<String, Object> paramMap = new HashMap<>();
|
||||
paramMap.put("model", config.getModel());
|
||||
paramMap.put("text", text);
|
||||
return JSONUtil.toJsonStr(paramMap);
|
||||
}
|
||||
|
||||
//构建批量推理chat请求体
|
||||
private String buildBatchChatRequestBody(final List<Message> messages) {
|
||||
return buildChatRequestBody(messages);
|
||||
}
|
||||
|
||||
private Map<String, Object> buildBatchChatStreamRequestBody(final List<Message> messages) {
|
||||
return buildChatStreamRequestBody(messages);
|
||||
}
|
||||
|
||||
//构建创建上下文缓存请求体
|
||||
private String buildCreateContextRequest(final List<Message> messages, String mode) {
|
||||
final Map<String, Object> paramMap = new HashMap<>();
|
||||
paramMap.put("messages", messages);
|
||||
paramMap.put("model", config.getModel());
|
||||
paramMap.put("mode", mode);
|
||||
//合并其他参数
|
||||
paramMap.putAll(config.getAdditionalConfigMap());
|
||||
|
||||
return JSONUtil.toJsonStr(paramMap);
|
||||
}
|
||||
|
||||
//构建上下文缓存对话请求体
|
||||
private String buildChatContentRequestBody(final List<Message> messages, String contextId) {
|
||||
//使用JSON工具
|
||||
final Map<String, Object> paramMap = new HashMap<>();
|
||||
paramMap.put("model", config.getModel());
|
||||
paramMap.put("messages", messages);
|
||||
paramMap.put("context_id", contextId);
|
||||
//合并其他参数
|
||||
paramMap.putAll(config.getAdditionalConfigMap());
|
||||
|
||||
return JSONUtil.toJsonStr(paramMap);
|
||||
}
|
||||
|
||||
private Map<String, Object> buildChatContentStreamRequestBody(final List<Message> messages, String contextId) {
|
||||
//使用JSON工具
|
||||
final Map<String, Object> paramMap = new HashMap<>();
|
||||
paramMap.put("stream", true);
|
||||
paramMap.put("model", config.getModel());
|
||||
paramMap.put("messages", messages);
|
||||
paramMap.put("context_id", contextId);
|
||||
//合并其他参数
|
||||
paramMap.putAll(config.getAdditionalConfigMap());
|
||||
|
||||
return paramMap;
|
||||
}
|
||||
|
||||
//构建创建视频任务请求体
|
||||
private String buildGenerationsTasksRequestBody(String text, String image, final List<DoubaoCommon.DoubaoVideo> videoParams) {
|
||||
//使用JSON工具
|
||||
final Map<String, Object> paramMap = new HashMap<>();
|
||||
paramMap.put("model", config.getModel());
|
||||
|
||||
final List<Object> content = new ArrayList<>();
|
||||
//添加文本参数
|
||||
final Map<String, String> textMap = new HashMap<>();
|
||||
if (!StrUtil.isBlank(text)) {
|
||||
textMap.put("type", "text");
|
||||
textMap.put("text", text);
|
||||
content.add(textMap);
|
||||
}
|
||||
//添加图片参数
|
||||
if (!StrUtil.isBlank(image)) {
|
||||
final Map<String, Object> imgUrlMap = new HashMap<>();
|
||||
imgUrlMap.put("type", "image_url");
|
||||
final Map<String, String> urlMap = new HashMap<>();
|
||||
urlMap.put("url", image);
|
||||
imgUrlMap.put("image_url", urlMap);
|
||||
content.add(imgUrlMap);
|
||||
}
|
||||
|
||||
//添加视频参数
|
||||
if (videoParams != null && !videoParams.isEmpty()) {
|
||||
//如果有文本参数就加在后面
|
||||
if (textMap != null && !textMap.isEmpty()) {
|
||||
int textIndex = content.indexOf(textMap);
|
||||
StringBuilder textBuilder = new StringBuilder(text);
|
||||
for (DoubaoCommon.DoubaoVideo videoParam : videoParams) {
|
||||
textBuilder.append(" ").append(videoParam.getType()).append(" ").append(videoParam.getValue());
|
||||
}
|
||||
textMap.put("type", "text");
|
||||
textMap.put("text", textBuilder.toString());
|
||||
|
||||
if (textIndex != -1) {
|
||||
content.set(textIndex, textMap);
|
||||
} else {
|
||||
content.add(textMap);
|
||||
}
|
||||
} else {
|
||||
//如果没有文本参数就重新增加
|
||||
StringBuilder textBuilder = new StringBuilder();
|
||||
for (DoubaoCommon.DoubaoVideo videoParam : videoParams) {
|
||||
textBuilder.append(videoParam.getType()).append(videoParam.getValue()).append(" ");
|
||||
}
|
||||
textMap.put("type", "text");
|
||||
textMap.put("text", textBuilder.toString());
|
||||
content.add(textMap);
|
||||
}
|
||||
}
|
||||
|
||||
paramMap.put("content", content);
|
||||
//合并其他参数
|
||||
paramMap.putAll(config.getAdditionalConfigMap());
|
||||
return JSONUtil.toJsonStr(paramMap);
|
||||
}
|
||||
|
||||
//构建文生图请求体
|
||||
private String buildImagesGenerationsRequestBody(String prompt) {
|
||||
final Map<String, Object> paramMap = new HashMap<>();
|
||||
paramMap.put("model", config.getModel());
|
||||
paramMap.put("prompt", prompt);
|
||||
//合并其他参数
|
||||
paramMap.putAll(config.getAdditionalConfigMap());
|
||||
|
||||
return JSONUtil.toJsonStr(paramMap);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Hutool Team and hutool.cn
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* 对doubao的封装实现
|
||||
*
|
||||
* @author elichow
|
||||
* @since 5.8.38
|
||||
*/
|
||||
|
||||
package cn.hutool.ai.model.doubao;
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Hutool Team and hutool.cn
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package cn.hutool.ai.model.grok;
|
||||
|
||||
/**
|
||||
* grok公共类
|
||||
*
|
||||
* @author elichow
|
||||
* @since 5.8.38
|
||||
*/
|
||||
public class GrokCommon {
|
||||
|
||||
//grok视觉参数
|
||||
public enum GrokVision {
|
||||
|
||||
AUTO("auto"),
|
||||
LOW("low"),
|
||||
HIGH("high");
|
||||
|
||||
private final String detail;
|
||||
|
||||
GrokVision(String detail) {
|
||||
this.detail = detail;
|
||||
}
|
||||
|
||||
public String getDetail() {
|
||||
return detail;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Hutool Team and hutool.cn
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package cn.hutool.ai.model.grok;
|
||||
|
||||
import cn.hutool.ai.Models;
|
||||
import cn.hutool.ai.core.BaseConfig;
|
||||
|
||||
/**
|
||||
* Grok配置类,初始化API接口地址,设置默认的模型
|
||||
*
|
||||
* @author elichow
|
||||
* @since 5.8.38
|
||||
*/
|
||||
public class GrokConfig extends BaseConfig {
|
||||
|
||||
private final String API_URL = "https://api.x.ai/v1";
|
||||
|
||||
private final String DEFAULT_MODEL = Models.Grok.GROK_2_1212.getModel();
|
||||
|
||||
|
||||
public GrokConfig() {
|
||||
setApiUrl(API_URL);
|
||||
setModel(DEFAULT_MODEL);
|
||||
}
|
||||
|
||||
public GrokConfig(String apiKey) {
|
||||
this();
|
||||
setApiKey(apiKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getModelName() {
|
||||
return "grok";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Hutool Team and hutool.cn
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package cn.hutool.ai.model.grok;
|
||||
|
||||
import cn.hutool.ai.core.AIConfig;
|
||||
import cn.hutool.ai.core.AIServiceProvider;
|
||||
|
||||
/**r
|
||||
* 创建Grok服务实现类
|
||||
*
|
||||
* @author elichow
|
||||
* @since 5.8.38
|
||||
*/
|
||||
public class GrokProvider implements AIServiceProvider {
|
||||
|
||||
@Override
|
||||
public String getServiceName() {
|
||||
return "grok";
|
||||
}
|
||||
|
||||
@Override
|
||||
public GrokService create(final AIConfig config) {
|
||||
return new GrokServiceImpl(config);
|
||||
}
|
||||
}
|
||||
192
hutool-ai/src/main/java/cn/hutool/ai/model/grok/GrokService.java
Normal file
192
hutool-ai/src/main/java/cn/hutool/ai/model/grok/GrokService.java
Normal file
@@ -0,0 +1,192 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Hutool Team and hutool.cn
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package cn.hutool.ai.model.grok;
|
||||
|
||||
import cn.hutool.ai.core.AIService;
|
||||
import cn.hutool.ai.core.Message;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* grok支持的扩展接口
|
||||
*
|
||||
* @author elichow
|
||||
* @since 5.8.38
|
||||
*/
|
||||
public interface GrokService extends AIService {
|
||||
|
||||
/**
|
||||
* 创建消息回复
|
||||
*
|
||||
* @param prompt 题词
|
||||
* @param maxToken 最大token
|
||||
* @return AI回答
|
||||
* @since 5.8.38
|
||||
*/
|
||||
default String message(String prompt, int maxToken){
|
||||
// 定义消息结构
|
||||
final List<Message> messages = new ArrayList<>();
|
||||
messages.add(new Message("system", "You are a helpful assistant"));
|
||||
messages.add(new Message("user", prompt));
|
||||
return message(messages, maxToken);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建消息回复-SSE流式输出
|
||||
*
|
||||
* @param prompt 题词
|
||||
* @param maxToken 最大token
|
||||
* @param callback 流式数据回调函数
|
||||
* @since 5.8.39
|
||||
*/
|
||||
default void message(String prompt, int maxToken, final Consumer<String> callback){
|
||||
final List<Message> messages = new ArrayList<>();
|
||||
messages.add(new Message("system", "You are a helpful assistant"));
|
||||
messages.add(new Message("user", prompt));
|
||||
message(messages, maxToken, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建消息回复
|
||||
*
|
||||
* @param messages messages 由对话组成的消息列表。如系统人设,背景信息等,用户自定义的信息
|
||||
* @param maxToken 最大token
|
||||
* @return AI回答
|
||||
* @since 5.8.39
|
||||
*/
|
||||
String message(List<Message> messages, int maxToken);
|
||||
|
||||
/**
|
||||
* 创建消息回复-SSE流式输出
|
||||
*
|
||||
* @param messages messages 由对话组成的消息列表。如系统人设,背景信息等,用户自定义的信息
|
||||
* @param maxToken 最大token
|
||||
* @param callback 流式数据回调函数
|
||||
* @since 5.8.39
|
||||
*/
|
||||
void message(List<Message> messages, int maxToken, final Consumer<String> callback);
|
||||
|
||||
/**
|
||||
* 图像理解:模型会依据传入的图片信息以及问题,给出回复。
|
||||
*
|
||||
* @param prompt 题词
|
||||
* @param images 图片列表/或者图片Base64编码图片列表(URI形式)
|
||||
* @param detail 手动设置图片的质量,取值范围high、low、auto,默认为auto
|
||||
* @return AI回答
|
||||
* @since 5.8.38
|
||||
*/
|
||||
String chatVision(String prompt, final List<String> images, String detail);
|
||||
|
||||
/**
|
||||
* 图像理解-SSE流式输出
|
||||
*
|
||||
* @param prompt 题词
|
||||
* @param images 图片列表/或者图片Base64编码图片列表(URI形式)
|
||||
* @param detail 手动设置图片的质量,取值范围high、low、auto,默认为auto
|
||||
* @param callback 流式数据回调函数
|
||||
* @since 5.8.39
|
||||
*/
|
||||
void chatVision(String prompt, final List<String> images, String detail,final Consumer<String> callback);
|
||||
|
||||
/**
|
||||
* 图像理解:模型会依据传入的图片信息以及问题,给出回复。
|
||||
*
|
||||
* @param prompt 题词
|
||||
* @param images 传入的图片列表地址/或者图片Base64编码图片列表(URI形式)
|
||||
* @return AI回答
|
||||
* @since 5.8.38
|
||||
*/
|
||||
default String chatVision(String prompt, final List<String> images) {
|
||||
return chatVision(prompt, images, GrokCommon.GrokVision.AUTO.getDetail());
|
||||
}
|
||||
|
||||
/**
|
||||
* 图像理解:模型会依据传入的图片信息以及问题,给出回复。
|
||||
*
|
||||
* @param prompt 题词
|
||||
* @param images 传入|的图片列表地址/或者图片Base64编码图片列表(URI形式)
|
||||
* @param callback 流式数据回调函数
|
||||
* @since 5.8.39
|
||||
*/
|
||||
default void chatVision(String prompt, final List<String> images, final Consumer<String> callback){
|
||||
chatVision(prompt, images, GrokCommon.GrokVision.AUTO.getDetail(), callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* 列出所有model列表
|
||||
*
|
||||
* @return model列表
|
||||
* @since 5.8.38
|
||||
*/
|
||||
String models();
|
||||
|
||||
/**
|
||||
* 获取模型信息
|
||||
*
|
||||
* @param modelId model ID
|
||||
* @return model信息
|
||||
* @since 5.8.38
|
||||
*/
|
||||
String getModel(String modelId);
|
||||
|
||||
/**
|
||||
* 列出所有语言model
|
||||
*
|
||||
* @return languageModel列表
|
||||
* @since 5.8.38
|
||||
*/
|
||||
String languageModels();
|
||||
|
||||
/**
|
||||
* 获取语言模型信息
|
||||
*
|
||||
* @param modelId model ID
|
||||
* @return model信息
|
||||
* @since 5.8.38
|
||||
*/
|
||||
String getLanguageModel(String modelId);
|
||||
|
||||
/**
|
||||
* 分词:可以将文本转换为模型可理解的 token 信息
|
||||
*
|
||||
* @param text 需要分词的内容
|
||||
* @return 分词结果
|
||||
* @since 5.8.38
|
||||
*/
|
||||
String tokenizeText(String text);
|
||||
|
||||
/**
|
||||
* 从延迟对话中获取结果
|
||||
*
|
||||
* @param requestId 延迟对话中的延迟请求ID
|
||||
* @return AI回答
|
||||
* @since 5.8.38
|
||||
*/
|
||||
String deferredCompletion(String requestId);
|
||||
|
||||
/**
|
||||
* 文生图
|
||||
* 请设置config中model为支持图片功能的模型,目前支持GROK_2_IMAGE
|
||||
*
|
||||
* @param prompt 题词
|
||||
* @return 包含生成图片的url
|
||||
* @since 5.8.39
|
||||
*/
|
||||
String imagesGenerations(String prompt);
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Hutool Team and hutool.cn
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package cn.hutool.ai.model.grok;
|
||||
|
||||
import cn.hutool.ai.core.AIConfig;
|
||||
import cn.hutool.ai.core.BaseAIService;
|
||||
import cn.hutool.ai.core.Message;
|
||||
import cn.hutool.core.thread.ThreadUtil;
|
||||
import cn.hutool.http.HttpResponse;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* Grok服务,AI具体功能的实现
|
||||
*
|
||||
* @author elichow
|
||||
* @since 5.8.38
|
||||
*/
|
||||
public class GrokServiceImpl extends BaseAIService implements GrokService {
|
||||
|
||||
//对话补全
|
||||
private final String CHAT_ENDPOINT = "/chat/completions";
|
||||
//创建消息回复
|
||||
private final String MESSAGES = "/messages";
|
||||
//列出模型
|
||||
private final String MODELS_ENDPOINT = "/models";
|
||||
//列出语言模型
|
||||
private final String LANGUAGE_MODELS = "/language-models";
|
||||
//分词
|
||||
private final String TOKENIZE_TEXT = "/tokenize-text";
|
||||
//获取延迟对话
|
||||
private final String DEFERRED_COMPLETION = "/chat/deferred-completion";
|
||||
//文生图
|
||||
private final String IMAGES_GENERATIONS = "/images/generations";
|
||||
|
||||
public GrokServiceImpl(final AIConfig config) {
|
||||
//初始化grok客户端
|
||||
super(config);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String chat(final List<Message> messages) {
|
||||
String paramJson = buildChatRequestBody(messages);
|
||||
final HttpResponse response = sendPost(CHAT_ENDPOINT, paramJson);
|
||||
return response.body();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void chat(List<Message> messages,Consumer<String> callback) {
|
||||
Map<String, Object> paramMap = buildChatStreamRequestBody(messages);
|
||||
ThreadUtil.newThread(() -> sendPostStream(CHAT_ENDPOINT, paramMap, callback::accept), "grok-chat-sse").start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String message(final List<Message> messages, int maxToken) {
|
||||
String paramJson = buildMessageRequestBody(messages, maxToken);
|
||||
final HttpResponse response = sendPost(MESSAGES, paramJson);
|
||||
return response.body();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void message(List<Message> messages, int maxToken, final Consumer<String> callback) {
|
||||
Map<String, Object> paramMap = buildMessageStreamRequestBody(messages, maxToken);
|
||||
ThreadUtil.newThread(() -> sendPostStream(MESSAGES, paramMap, callback::accept), "grok-message-sse").start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String chatVision(String prompt, final List<String> images, String detail) {
|
||||
String paramJson = buildChatVisionRequestBody(prompt, images, detail);
|
||||
final HttpResponse response = sendPost(CHAT_ENDPOINT, paramJson);
|
||||
return response.body();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void chatVision(String prompt, List<String> images, String detail, Consumer<String> callback) {
|
||||
Map<String, Object> paramMap = buildChatVisionStreamRequestBody(prompt, images, detail);
|
||||
ThreadUtil.newThread(() -> sendPostStream(CHAT_ENDPOINT, paramMap, callback::accept), "grok-chatVision-sse").start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String models() {
|
||||
final HttpResponse response = sendGet(MODELS_ENDPOINT);
|
||||
return response.body();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getModel(String modelId) {
|
||||
final HttpResponse response = sendGet(MODELS_ENDPOINT + "/" + modelId);
|
||||
return response.body();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String languageModels() {
|
||||
final HttpResponse response = sendGet(LANGUAGE_MODELS);
|
||||
return response.body();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getLanguageModel(String modelId) {
|
||||
final HttpResponse response = sendGet(LANGUAGE_MODELS + "/" + modelId);
|
||||
return response.body();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String tokenizeText(String text) {
|
||||
String paramJson = buildTokenizeRequestBody(text);
|
||||
final HttpResponse response = sendPost(TOKENIZE_TEXT, paramJson);
|
||||
return response.body();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String deferredCompletion(String requestId) {
|
||||
final HttpResponse response = sendGet(DEFERRED_COMPLETION + "/" + requestId);
|
||||
return response.body();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String imagesGenerations(String prompt) {
|
||||
String paramJson = buildImagesGenerationsRequestBody(prompt);
|
||||
final HttpResponse response = sendPost(IMAGES_GENERATIONS, paramJson);
|
||||
return response.body();
|
||||
}
|
||||
|
||||
// 构建chat请求体
|
||||
private String buildChatRequestBody(final List<Message> messages) {
|
||||
//使用JSON工具
|
||||
final Map<String, Object> paramMap = new HashMap<>();
|
||||
paramMap.put("model", config.getModel());
|
||||
paramMap.put("messages", messages);
|
||||
//合并其他参数
|
||||
paramMap.putAll(config.getAdditionalConfigMap());
|
||||
|
||||
return JSONUtil.toJsonStr(paramMap);
|
||||
}
|
||||
|
||||
private Map<String, Object> buildChatStreamRequestBody(final List<Message> messages) {
|
||||
//使用JSON工具
|
||||
final Map<String, Object> paramMap = new HashMap<>();
|
||||
paramMap.put("stream", true);
|
||||
paramMap.put("model", config.getModel());
|
||||
paramMap.put("messages", messages);
|
||||
//合并其他参数
|
||||
paramMap.putAll(config.getAdditionalConfigMap());
|
||||
|
||||
return paramMap;
|
||||
}
|
||||
|
||||
//构建chatVision请求体
|
||||
private String buildChatVisionRequestBody(String prompt, final List<String> images, String detail) {
|
||||
// 定义消息结构
|
||||
final List<Message> messages = new ArrayList<>();
|
||||
final List<Object> content = new ArrayList<>();
|
||||
|
||||
final Map<String, String> contentMap = new HashMap<>();
|
||||
contentMap.put("type", "text");
|
||||
contentMap.put("text", prompt);
|
||||
content.add(contentMap);
|
||||
for (String img : images) {
|
||||
HashMap<String, Object> imgUrlMap = new HashMap<>();
|
||||
imgUrlMap.put("type", "image_url");
|
||||
HashMap<String, String> urlMap = new HashMap<>();
|
||||
urlMap.put("url", img);
|
||||
urlMap.put("detail", detail);
|
||||
imgUrlMap.put("image_url", urlMap);
|
||||
content.add(imgUrlMap);
|
||||
}
|
||||
|
||||
messages.add(new Message("user", content));
|
||||
|
||||
//使用JSON工具
|
||||
final Map<String, Object> paramMap = new HashMap<>();
|
||||
paramMap.put("model", config.getModel());
|
||||
paramMap.put("messages", messages);
|
||||
//合并其他参数
|
||||
paramMap.putAll(config.getAdditionalConfigMap());
|
||||
return JSONUtil.toJsonStr(paramMap);
|
||||
}
|
||||
|
||||
private Map<String, Object> buildChatVisionStreamRequestBody(String prompt, final List<String> images, String detail) {
|
||||
// 定义消息结构
|
||||
final List<Message> messages = new ArrayList<>();
|
||||
final List<Object> content = new ArrayList<>();
|
||||
|
||||
final Map<String, String> contentMap = new HashMap<>();
|
||||
contentMap.put("type", "text");
|
||||
contentMap.put("text", prompt);
|
||||
content.add(contentMap);
|
||||
for (String img : images) {
|
||||
HashMap<String, Object> imgUrlMap = new HashMap<>();
|
||||
imgUrlMap.put("type", "image_url");
|
||||
HashMap<String, String> urlMap = new HashMap<>();
|
||||
urlMap.put("url", img);
|
||||
urlMap.put("detail", detail);
|
||||
imgUrlMap.put("image_url", urlMap);
|
||||
content.add(imgUrlMap);
|
||||
}
|
||||
|
||||
messages.add(new Message("user", content));
|
||||
|
||||
//使用JSON工具
|
||||
final Map<String, Object> paramMap = new HashMap<>();
|
||||
paramMap.put("stream", true);
|
||||
paramMap.put("model", config.getModel());
|
||||
paramMap.put("messages", messages);
|
||||
//合并其他参数
|
||||
paramMap.putAll(config.getAdditionalConfigMap());
|
||||
return paramMap;
|
||||
}
|
||||
|
||||
//构建消息回复请求体
|
||||
private String buildMessageRequestBody(final List<Message> messages, int maxToken) {
|
||||
final Map<String, Object> paramMap = new HashMap<>();
|
||||
paramMap.put("model", config.getModel());
|
||||
paramMap.put("messages", messages);
|
||||
paramMap.put("max_tokens", maxToken);
|
||||
//合并其他参数
|
||||
paramMap.putAll(config.getAdditionalConfigMap());
|
||||
|
||||
return JSONUtil.toJsonStr(paramMap);
|
||||
}
|
||||
|
||||
private Map<String, Object> buildMessageStreamRequestBody(final List<Message> messages, int maxToken) {
|
||||
final Map<String, Object> paramMap = new HashMap<>();
|
||||
paramMap.put("stream", true);
|
||||
paramMap.put("model", config.getModel());
|
||||
paramMap.put("messages", messages);
|
||||
paramMap.put("max_tokens", maxToken);
|
||||
//合并其他参数
|
||||
paramMap.putAll(config.getAdditionalConfigMap());
|
||||
|
||||
return paramMap;
|
||||
}
|
||||
|
||||
//构建分词请求体
|
||||
private String buildTokenizeRequestBody(String text) {
|
||||
//使用JSON工具
|
||||
final Map<String, Object> paramMap = new HashMap<>();
|
||||
paramMap.put("model", config.getModel());
|
||||
paramMap.put("text", text);
|
||||
//合并其他参数
|
||||
paramMap.putAll(config.getAdditionalConfigMap());
|
||||
|
||||
return JSONUtil.toJsonStr(paramMap);
|
||||
}
|
||||
|
||||
//构建文生图请求体
|
||||
private String buildImagesGenerationsRequestBody(String prompt) {
|
||||
final Map<String, Object> paramMap = new HashMap<>();
|
||||
paramMap.put("model", config.getModel());
|
||||
paramMap.put("prompt", prompt);
|
||||
//合并其他参数
|
||||
paramMap.putAll(config.getAdditionalConfigMap());
|
||||
|
||||
return JSONUtil.toJsonStr(paramMap);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Hutool Team and hutool.cn
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* 对grok的封装实现
|
||||
*
|
||||
* @author elichow
|
||||
* @since 5.8.38
|
||||
*/
|
||||
|
||||
package cn.hutool.ai.model.grok;
|
||||
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Hutool Team and hutool.cn
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package cn.hutool.ai.model.hutool;
|
||||
|
||||
/**
|
||||
* hutool公共类
|
||||
*
|
||||
* @author elichow
|
||||
* @since 5.8.39
|
||||
*/
|
||||
public class HutoolCommon {
|
||||
|
||||
//hutool视觉参数
|
||||
public enum HutoolVision {
|
||||
|
||||
AUTO("auto"),
|
||||
LOW("low"),
|
||||
HIGH("high");
|
||||
|
||||
private final String detail;
|
||||
|
||||
HutoolVision(String detail) {
|
||||
this.detail = detail;
|
||||
}
|
||||
|
||||
public String getDetail() {
|
||||
return detail;
|
||||
}
|
||||
}
|
||||
|
||||
//hutool音频参数
|
||||
public enum HutoolSpeech {
|
||||
|
||||
ALLOY("alloy"),
|
||||
ASH("ash"),
|
||||
CORAL("coral"),
|
||||
ECHO("echo"),
|
||||
FABLE("fable"),
|
||||
ONYX("onyx"),
|
||||
NOVA("nova"),
|
||||
SAGE("sage"),
|
||||
SHIMMER("shimmer");
|
||||
|
||||
private final String voice;
|
||||
|
||||
HutoolSpeech(String voice) {
|
||||
this.voice = voice;
|
||||
}
|
||||
|
||||
public String getVoice() {
|
||||
return voice;
|
||||
}
|
||||
}
|
||||
|
||||
//hutool视频生成参数
|
||||
public enum HutoolVideo {
|
||||
|
||||
//宽高比例
|
||||
RATIO_16_9("--rt", "16:9"),//[1280, 720]
|
||||
RATIO_4_3("--rt", "4:3"),//[960, 720]
|
||||
RATIO_1_1("--rt", "1:1"),//[720, 720]
|
||||
RATIO_3_4("--rt", "3:4"),//[720, 960]
|
||||
RATIO_9_16("--rt", "9:16"),//[720, 1280]
|
||||
RATIO_21_9("--rt", "21:9"),//[1280, 544]
|
||||
|
||||
//生成视频时长
|
||||
DURATION_5("--dur", 5),//文生视频,图生视频
|
||||
DURATION_10("--dur", 10),//文生视频
|
||||
|
||||
//帧率,即一秒时间内视频画面数量
|
||||
FPS_5("--fps", 24),
|
||||
|
||||
//视频分辨率
|
||||
RESOLUTION_5("--rs", "720p"),
|
||||
|
||||
//生成视频是否包含水印
|
||||
WATERMARK_TRUE("--wm", true),
|
||||
WATERMARK_FALSE("--wm", false);
|
||||
|
||||
private final String type;
|
||||
private final Object value;
|
||||
|
||||
HutoolVideo(String type, Object value) {
|
||||
this.type = type;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public Object getValue() {
|
||||
if (value instanceof String) {
|
||||
return (String) value;
|
||||
} else if (value instanceof Integer) {
|
||||
return (Integer) value;
|
||||
} else if (value instanceof Boolean) {
|
||||
return (Boolean) value;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Hutool Team and hutool.cn
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package cn.hutool.ai.model.hutool;
|
||||
|
||||
import cn.hutool.ai.Models;
|
||||
import cn.hutool.ai.core.BaseConfig;
|
||||
|
||||
/**
|
||||
* Hutool配置类,初始化API接口地址,设置默认的模型
|
||||
*
|
||||
* @author elichow
|
||||
* @since 5.8.39
|
||||
*/
|
||||
public class HutoolConfig extends BaseConfig {
|
||||
|
||||
private final String API_URL = "https://api.hutool.cn/ai/api";
|
||||
|
||||
private final String DEFAULT_MODEL = Models.Hutool.HUTOOL.getModel();
|
||||
|
||||
public HutoolConfig() {
|
||||
setApiUrl(API_URL);
|
||||
setModel(DEFAULT_MODEL);
|
||||
}
|
||||
|
||||
public HutoolConfig(String apiKey) {
|
||||
this();
|
||||
setApiKey(apiKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getModelName() {
|
||||
return "hutool";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Hutool Team and hutool.cn
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package cn.hutool.ai.model.hutool;
|
||||
|
||||
import cn.hutool.ai.core.AIConfig;
|
||||
import cn.hutool.ai.core.AIServiceProvider;
|
||||
|
||||
/**r
|
||||
* 创建Hutool服务实现类
|
||||
*
|
||||
* @author elichow
|
||||
* @since 5.8.39
|
||||
*/
|
||||
public class HutoolProvider implements AIServiceProvider {
|
||||
|
||||
@Override
|
||||
public String getServiceName() {
|
||||
return "hutool";
|
||||
}
|
||||
|
||||
@Override
|
||||
public HutoolService create(final AIConfig config) {
|
||||
return new HutoolServiceImpl(config);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Hutool Team and hutool.cn
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package cn.hutool.ai.model.hutool;
|
||||
|
||||
import cn.hutool.ai.core.AIService;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.InputStream;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* hutool支持的扩展接口
|
||||
*
|
||||
* @author elichow
|
||||
* @since 5.8.39
|
||||
*/
|
||||
public interface HutoolService extends AIService {
|
||||
|
||||
/**
|
||||
* 图像理解:模型会依据传入的图片信息以及问题,给出回复。
|
||||
*
|
||||
* @param prompt 题词
|
||||
* @param images 图片列表/或者图片Base64编码图片列表(URI形式)
|
||||
* @param detail 手动设置图片的质量,取值范围high、low、auto,默认为auto
|
||||
* @return AI回答
|
||||
* @since 5.8.39
|
||||
*/
|
||||
String chatVision(String prompt, final List<String> images, String detail);
|
||||
|
||||
/**
|
||||
* 图像理解-SSE流式输出
|
||||
*
|
||||
* @param prompt 题词
|
||||
* @param images 图片列表/或者图片Base64编码图片列表(URI形式)
|
||||
* @param detail 手动设置图片的质量,取值范围high、low、auto,默认为auto
|
||||
* @param callback 流式数据回调函数
|
||||
* @since 5.8.39
|
||||
*/
|
||||
void chatVision(String prompt, final List<String> images, String detail,final Consumer<String> callback);
|
||||
|
||||
/**
|
||||
* 图像理解:模型会依据传入的图片信息以及问题,给出回复。
|
||||
*
|
||||
* @param prompt 题词
|
||||
* @param images 传入的图片列表地址/或者图片Base64编码图片列表(URI形式)
|
||||
* @return AI回答
|
||||
* @since 5.8.39
|
||||
*/
|
||||
default String chatVision(String prompt, final List<String> images) {
|
||||
return chatVision(prompt, images, HutoolCommon.HutoolVision.AUTO.getDetail());
|
||||
}
|
||||
|
||||
/**
|
||||
* 图像理解:模型会依据传入的图片信息以及问题,给出回复。
|
||||
*
|
||||
* @param prompt 题词
|
||||
* @param images 传入|的图片列表地址/或者图片Base64编码图片列表(URI形式)
|
||||
* @param callback 流式数据回调函数
|
||||
* @since 5.8.39
|
||||
*/
|
||||
default void chatVision(String prompt, final List<String> images, final Consumer<String> callback){
|
||||
chatVision(prompt, images, HutoolCommon.HutoolVision.AUTO.getDetail(), callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* 分词:可以将文本转换为模型可理解的 token 信息
|
||||
*
|
||||
* @param text 需要分词的内容
|
||||
* @return 分词结果
|
||||
* @since 5.8.39
|
||||
*/
|
||||
String tokenizeText(String text);
|
||||
|
||||
/**
|
||||
* 文生图
|
||||
*
|
||||
* @param prompt 题词
|
||||
* @return 包含生成图片的url
|
||||
* @since 5.8.39
|
||||
*/
|
||||
String imagesGenerations(String prompt);
|
||||
|
||||
/**
|
||||
* 图文向量化:仅支持单一文本、单张图片或文本与图片的组合输入(即一段文本 + 一张图片),暂不支持批量文本 / 图片的同时处理
|
||||
*
|
||||
* @param text 需要向量化的内容
|
||||
* @param image 需要向量化的图片地址/或者图片Base64编码图片(URI形式)
|
||||
* @return 处理后的向量信息
|
||||
* @since 5.8.39
|
||||
*/
|
||||
String embeddingVision(String text, String image);
|
||||
|
||||
/**
|
||||
* TTS文本转语音
|
||||
*
|
||||
* @param input 需要转成语音的文本
|
||||
* @param voice AI的音色
|
||||
* @return 返回的音频mp3文件流
|
||||
* @since 5.8.39
|
||||
*/
|
||||
InputStream tts(String input, final HutoolCommon.HutoolSpeech voice);
|
||||
|
||||
/**
|
||||
* TTS文本转语音
|
||||
*
|
||||
* @param input 需要转成语音的文本
|
||||
* @return 返回的音频mp3文件流
|
||||
* @since 5.8.39
|
||||
*/
|
||||
default InputStream tts(String input) {
|
||||
return tts(input, HutoolCommon.HutoolSpeech.ALLOY);
|
||||
}
|
||||
|
||||
/**
|
||||
* STT音频转文本
|
||||
*
|
||||
* @param file 需要转成文本的音频文件
|
||||
* @return 返回的文本内容
|
||||
* @since 5.8.39
|
||||
*/
|
||||
String stt(final File file);
|
||||
|
||||
/**
|
||||
* 创建视频生成任务
|
||||
*
|
||||
* @param text 文本提示词
|
||||
* @param image 图片/或者图片Base64编码图片(URI形式)
|
||||
* @param videoParams 视频参数列表
|
||||
* @return 生成任务id
|
||||
* @since 5.8.39
|
||||
*/
|
||||
String videoTasks(String text, String image, final List<HutoolCommon.HutoolVideo> videoParams);
|
||||
|
||||
/**
|
||||
* 创建视频生成任务
|
||||
*
|
||||
* @param text 文本提示词
|
||||
* @param image 图片/或者图片Base64编码图片(URI形式)
|
||||
* @return 生成任务id
|
||||
* @since 5.8.39
|
||||
*/
|
||||
default String videoTasks(String text, String image) {
|
||||
return videoTasks(text, image, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询视频生成任务信息
|
||||
*
|
||||
* @param taskId 通过创建生成视频任务返回的生成任务id
|
||||
* @return 生成任务信息
|
||||
* @since 5.8.39
|
||||
*/
|
||||
String getVideoTasksInfo(String taskId);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,380 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Hutool Team and hutool.cn
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package cn.hutool.ai.model.hutool;
|
||||
|
||||
import cn.hutool.ai.AIException;
|
||||
import cn.hutool.ai.core.AIConfig;
|
||||
import cn.hutool.ai.core.BaseAIService;
|
||||
import cn.hutool.ai.core.Message;
|
||||
import cn.hutool.core.thread.ThreadUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.http.HttpResponse;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* Hutool服务,AI具体功能的实现
|
||||
*
|
||||
* @author elichow
|
||||
* @since 5.8.39
|
||||
*/
|
||||
public class HutoolServiceImpl extends BaseAIService implements HutoolService {
|
||||
|
||||
//对话补全
|
||||
private final String CHAT_ENDPOINT = "/chat/completions";
|
||||
//分词
|
||||
private final String TOKENIZE_TEXT = "/tokenize/text";
|
||||
//文生图
|
||||
private final String IMAGES_GENERATIONS = "/images/generations";
|
||||
//图文向量化
|
||||
private final String EMBEDDING_VISION = "/embeddings/multimodal";
|
||||
//文本转语音
|
||||
private final String TTS = "/audio/tts";
|
||||
//语音转文本
|
||||
private final String STT = "/audio/stt";
|
||||
//创建视频生成任务
|
||||
private final String CREATE_VIDEO = "/video/generations";
|
||||
|
||||
public HutoolServiceImpl(final AIConfig config) {
|
||||
//初始化hutool客户端
|
||||
super(config);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String chat(final List<Message> messages) {
|
||||
String paramJson = buildChatRequestBody(messages);
|
||||
final HttpResponse response = sendPost(CHAT_ENDPOINT, paramJson);
|
||||
return response.body();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void chat(List<Message> messages,Consumer<String> callback) {
|
||||
Map<String, Object> paramMap = buildChatStreamRequestBody(messages);
|
||||
ThreadUtil.newThread(() -> sendPostStream(CHAT_ENDPOINT, paramMap, callback::accept), "hutool-chat-sse").start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String chatVision(String prompt, final List<String> images, String detail) {
|
||||
String paramJson = buildChatVisionRequestBody(prompt, images, detail);
|
||||
final HttpResponse response = sendPost(CHAT_ENDPOINT, paramJson);
|
||||
return response.body();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void chatVision(String prompt, List<String> images, String detail, Consumer<String> callback) {
|
||||
Map<String, Object> paramMap = buildChatVisionStreamRequestBody(prompt, images, detail);
|
||||
System.out.println(JSONUtil.toJsonStr(paramMap));
|
||||
ThreadUtil.newThread(() -> sendPostStream(CHAT_ENDPOINT, paramMap, callback::accept), "hutool-chatVision-sse").start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String tokenizeText(String text) {
|
||||
String paramJson = buildTokenizeRequestBody(text);
|
||||
final HttpResponse response = sendPost(TOKENIZE_TEXT, paramJson);
|
||||
return response.body();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String imagesGenerations(String prompt) {
|
||||
String paramJson = buildImagesGenerationsRequestBody(prompt);
|
||||
final HttpResponse response = sendPost(IMAGES_GENERATIONS, paramJson);
|
||||
return response.body();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String embeddingVision(String text, String image) {
|
||||
String paramJson = buildEmbeddingVisionRequestBody(text, image);
|
||||
final HttpResponse response = sendPost(EMBEDDING_VISION, paramJson);
|
||||
return response.body();
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream tts(String input, final HutoolCommon.HutoolSpeech voice) {
|
||||
try {
|
||||
String paramJson = buildTTSRequestBody(input, voice.getVoice());
|
||||
final HttpResponse response = sendPost(TTS, paramJson);
|
||||
|
||||
// 检查响应内容类型
|
||||
String contentType = response.header("Content-Type");
|
||||
if (contentType != null && contentType.startsWith("application/json")) {
|
||||
// 如果是JSON响应,说明有错误
|
||||
String errorBody = response.body();
|
||||
throw new AIException("TTS请求失败: " + errorBody);
|
||||
}
|
||||
// 默认返回音频流
|
||||
return response.bodyStream();
|
||||
} catch (Exception e) {
|
||||
throw new AIException("TTS处理失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String stt(final File file) {
|
||||
final Map<String, Object> paramMap = buildSTTRequestBody(file);
|
||||
final HttpResponse response = sendFormData(STT, paramMap);
|
||||
return response.body();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String videoTasks(String text, String image, final List<HutoolCommon.HutoolVideo> videoParams) {
|
||||
String paramJson = buildGenerationsTasksRequestBody(text, image, videoParams);
|
||||
final HttpResponse response = sendPost(CREATE_VIDEO, paramJson);
|
||||
return response.body();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getVideoTasksInfo(String taskId) {
|
||||
final HttpResponse response = sendGet(CREATE_VIDEO + "/" + taskId);
|
||||
return response.body();
|
||||
}
|
||||
|
||||
|
||||
// 构建chat请求体
|
||||
private String buildChatRequestBody(final List<Message> messages) {
|
||||
//使用JSON工具
|
||||
final Map<String, Object> paramMap = new HashMap<>();
|
||||
paramMap.put("model", config.getModel());
|
||||
paramMap.put("messages", messages);
|
||||
//合并其他参数
|
||||
paramMap.putAll(config.getAdditionalConfigMap());
|
||||
|
||||
return JSONUtil.toJsonStr(paramMap);
|
||||
}
|
||||
|
||||
private Map<String, Object> buildChatStreamRequestBody(final List<Message> messages) {
|
||||
//使用JSON工具
|
||||
final Map<String, Object> paramMap = new HashMap<>();
|
||||
paramMap.put("stream", true);
|
||||
paramMap.put("model", config.getModel());
|
||||
paramMap.put("messages", messages);
|
||||
//合并其他参数
|
||||
paramMap.putAll(config.getAdditionalConfigMap());
|
||||
|
||||
return paramMap;
|
||||
}
|
||||
|
||||
//构建chatVision请求体
|
||||
private String buildChatVisionRequestBody(String prompt, final List<String> images, String detail) {
|
||||
// 定义消息结构
|
||||
final List<Message> messages = new ArrayList<>();
|
||||
final List<Object> content = new ArrayList<>();
|
||||
|
||||
final Map<String, String> contentMap = new HashMap<>();
|
||||
contentMap.put("type", "text");
|
||||
contentMap.put("text", prompt);
|
||||
content.add(contentMap);
|
||||
for (String img : images) {
|
||||
HashMap<String, Object> imgUrlMap = new HashMap<>();
|
||||
imgUrlMap.put("type", "image_url");
|
||||
HashMap<String, String> urlMap = new HashMap<>();
|
||||
urlMap.put("url", img);
|
||||
urlMap.put("detail", detail);
|
||||
imgUrlMap.put("image_url", urlMap);
|
||||
content.add(imgUrlMap);
|
||||
}
|
||||
|
||||
messages.add(new Message("user", content));
|
||||
|
||||
//使用JSON工具
|
||||
final Map<String, Object> paramMap = new HashMap<>();
|
||||
paramMap.put("model", config.getModel());
|
||||
paramMap.put("messages", messages);
|
||||
//合并其他参数
|
||||
paramMap.putAll(config.getAdditionalConfigMap());
|
||||
return JSONUtil.toJsonStr(paramMap);
|
||||
}
|
||||
|
||||
private Map<String, Object> buildChatVisionStreamRequestBody(String prompt, final List<String> images, String detail) {
|
||||
// 定义消息结构
|
||||
final List<Message> messages = new ArrayList<>();
|
||||
final List<Object> content = new ArrayList<>();
|
||||
|
||||
final Map<String, String> contentMap = new HashMap<>();
|
||||
contentMap.put("type", "text");
|
||||
contentMap.put("text", prompt);
|
||||
content.add(contentMap);
|
||||
for (String img : images) {
|
||||
HashMap<String, Object> imgUrlMap = new HashMap<>();
|
||||
imgUrlMap.put("type", "image_url");
|
||||
HashMap<String, String> urlMap = new HashMap<>();
|
||||
urlMap.put("url", img);
|
||||
urlMap.put("detail", detail);
|
||||
imgUrlMap.put("image_url", urlMap);
|
||||
content.add(imgUrlMap);
|
||||
}
|
||||
|
||||
messages.add(new Message("user", content));
|
||||
|
||||
//使用JSON工具
|
||||
final Map<String, Object> paramMap = new HashMap<>();
|
||||
paramMap.put("stream", true);
|
||||
paramMap.put("model", config.getModel());
|
||||
paramMap.put("messages", messages);
|
||||
//合并其他参数
|
||||
paramMap.putAll(config.getAdditionalConfigMap());
|
||||
return paramMap;
|
||||
}
|
||||
|
||||
|
||||
//构建分词请求体
|
||||
private String buildTokenizeRequestBody(String text) {
|
||||
//使用JSON工具
|
||||
final Map<String, Object> paramMap = new HashMap<>();
|
||||
paramMap.put("model", config.getModel());
|
||||
paramMap.put("text", text);
|
||||
//合并其他参数
|
||||
paramMap.putAll(config.getAdditionalConfigMap());
|
||||
|
||||
return JSONUtil.toJsonStr(paramMap);
|
||||
}
|
||||
|
||||
//构建文生图请求体
|
||||
private String buildImagesGenerationsRequestBody(String prompt) {
|
||||
final Map<String, Object> paramMap = new HashMap<>();
|
||||
paramMap.put("model", config.getModel());
|
||||
paramMap.put("prompt", prompt);
|
||||
//合并其他参数
|
||||
paramMap.putAll(config.getAdditionalConfigMap());
|
||||
|
||||
return JSONUtil.toJsonStr(paramMap);
|
||||
}
|
||||
|
||||
//构建图文向量化请求体
|
||||
private String buildEmbeddingVisionRequestBody(String text, String image) {
|
||||
//使用JSON工具
|
||||
final Map<String, Object> paramMap = new HashMap<>();
|
||||
paramMap.put("model", config.getModel());
|
||||
|
||||
final List<Object> input = new ArrayList<>();
|
||||
//添加文本参数
|
||||
if (!StrUtil.isBlank(text)) {
|
||||
final Map<String, String> textMap = new HashMap<>();
|
||||
textMap.put("type", "text");
|
||||
textMap.put("text", text);
|
||||
input.add(textMap);
|
||||
}
|
||||
//添加图片参数
|
||||
if (!StrUtil.isBlank(image)) {
|
||||
final Map<String, Object> imgUrlMap = new HashMap<>();
|
||||
imgUrlMap.put("type", "image_url");
|
||||
final Map<String, String> urlMap = new HashMap<>();
|
||||
urlMap.put("url", image);
|
||||
imgUrlMap.put("image_url", urlMap);
|
||||
input.add(imgUrlMap);
|
||||
}
|
||||
|
||||
paramMap.put("input", input);
|
||||
//合并其他参数
|
||||
paramMap.putAll(config.getAdditionalConfigMap());
|
||||
System.out.println(JSONUtil.toJsonStr(paramMap));
|
||||
return JSONUtil.toJsonStr(paramMap);
|
||||
}
|
||||
|
||||
|
||||
//构建TTS请求体
|
||||
private String buildTTSRequestBody(String input, String voice) {
|
||||
final Map<String, Object> paramMap = new HashMap<>();
|
||||
paramMap.put("model", config.getModel());
|
||||
paramMap.put("input", input);
|
||||
paramMap.put("voice", voice);
|
||||
//合并其他参数
|
||||
paramMap.putAll(config.getAdditionalConfigMap());
|
||||
|
||||
return JSONUtil.toJsonStr(paramMap);
|
||||
}
|
||||
|
||||
//构建STT请求体
|
||||
private Map<String, Object> buildSTTRequestBody(final File file) {
|
||||
final Map<String, Object> paramMap = new HashMap<>();
|
||||
paramMap.put("model", config.getModel());
|
||||
paramMap.put("file", file);
|
||||
//合并其他参数
|
||||
paramMap.putAll(config.getAdditionalConfigMap());
|
||||
|
||||
return paramMap;
|
||||
}
|
||||
|
||||
//构建创建视频任务请求体
|
||||
private String buildGenerationsTasksRequestBody(String text, String image, final List<HutoolCommon.HutoolVideo> videoParams) {
|
||||
//使用JSON工具
|
||||
final Map<String, Object> paramMap = new HashMap<>();
|
||||
paramMap.put("model", config.getModel());
|
||||
|
||||
final List<Object> content = new ArrayList<>();
|
||||
//添加文本参数
|
||||
final Map<String, String> textMap = new HashMap<>();
|
||||
if (!StrUtil.isBlank(text)) {
|
||||
textMap.put("type", "text");
|
||||
textMap.put("text", text);
|
||||
content.add(textMap);
|
||||
}
|
||||
//添加图片参数
|
||||
if (!StrUtil.isBlank(image)) {
|
||||
final Map<String, Object> imgUrlMap = new HashMap<>();
|
||||
imgUrlMap.put("type", "image_url");
|
||||
final Map<String, String> urlMap = new HashMap<>();
|
||||
urlMap.put("url", image);
|
||||
imgUrlMap.put("image_url", urlMap);
|
||||
content.add(imgUrlMap);
|
||||
}
|
||||
|
||||
//添加视频参数
|
||||
if (videoParams != null && !videoParams.isEmpty()) {
|
||||
//如果有文本参数就加在后面
|
||||
if (textMap != null && !textMap.isEmpty()) {
|
||||
int textIndex = content.indexOf(textMap);
|
||||
StringBuilder textBuilder = new StringBuilder(text);
|
||||
for (HutoolCommon.HutoolVideo videoParam : videoParams) {
|
||||
textBuilder.append(" ").append(videoParam.getType()).append(" ").append(videoParam.getValue());
|
||||
}
|
||||
textMap.put("type", "text");
|
||||
textMap.put("text", textBuilder.toString());
|
||||
|
||||
if (textIndex != -1) {
|
||||
content.set(textIndex, textMap);
|
||||
} else {
|
||||
content.add(textMap);
|
||||
}
|
||||
} else {
|
||||
//如果没有文本参数就重新增加
|
||||
StringBuilder textBuilder = new StringBuilder();
|
||||
for (HutoolCommon.HutoolVideo videoParam : videoParams) {
|
||||
textBuilder.append(videoParam.getType()).append(videoParam.getValue()).append(" ");
|
||||
}
|
||||
textMap.put("type", "text");
|
||||
textMap.put("text", textBuilder.toString());
|
||||
content.add(textMap);
|
||||
}
|
||||
}
|
||||
|
||||
paramMap.put("content", content);
|
||||
//合并其他参数
|
||||
paramMap.putAll(config.getAdditionalConfigMap());
|
||||
System.out.println(JSONUtil.toJsonStr(paramMap));
|
||||
return JSONUtil.toJsonStr(paramMap);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Hutool Team and hutool.cn
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* 对hutool的封装实现
|
||||
*
|
||||
* @author elichow
|
||||
* @since 5.8.39
|
||||
*/
|
||||
|
||||
package cn.hutool.ai.model.hutool;
|
||||
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Hutool Team and hutool.cn
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package cn.hutool.ai.model.openai;
|
||||
|
||||
/**
|
||||
* openai公共类
|
||||
*
|
||||
* @author elichow
|
||||
* @since 5.8.38
|
||||
*/
|
||||
public class OpenaiCommon {
|
||||
|
||||
//openai推理参数
|
||||
public enum OpenaiReasoning {
|
||||
|
||||
LOW("low"),
|
||||
MEDIUM("medium"),
|
||||
HIGH("high");
|
||||
|
||||
private final String effort;
|
||||
|
||||
OpenaiReasoning(String effort) {
|
||||
this.effort = effort;
|
||||
}
|
||||
|
||||
public String getEffort() {
|
||||
return effort;
|
||||
}
|
||||
}
|
||||
|
||||
//openai视觉参数
|
||||
public enum OpenaiVision {
|
||||
|
||||
AUTO("auto"),
|
||||
LOW("low"),
|
||||
HIGH("high");
|
||||
|
||||
private final String detail;
|
||||
|
||||
OpenaiVision(String detail) {
|
||||
this.detail = detail;
|
||||
}
|
||||
|
||||
public String getDetail() {
|
||||
return detail;
|
||||
}
|
||||
}
|
||||
|
||||
//openai音频参数
|
||||
public enum OpenaiSpeech {
|
||||
|
||||
ALLOY("alloy"),
|
||||
ASH("ash"),
|
||||
CORAL("coral"),
|
||||
ECHO("echo"),
|
||||
FABLE("fable"),
|
||||
ONYX("onyx"),
|
||||
NOVA("nova"),
|
||||
SAGE("sage"),
|
||||
SHIMMER("shimmer");
|
||||
|
||||
private final String voice;
|
||||
|
||||
OpenaiSpeech(String voice) {
|
||||
this.voice = voice;
|
||||
}
|
||||
|
||||
public String getVoice() {
|
||||
return voice;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Hutool Team and hutool.cn
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package cn.hutool.ai.model.openai;
|
||||
|
||||
import cn.hutool.ai.Models;
|
||||
import cn.hutool.ai.core.BaseConfig;
|
||||
|
||||
/**
|
||||
* openai配置类,初始化API接口地址,设置默认的模型
|
||||
*
|
||||
* @author elichow
|
||||
* @since 5.8.38
|
||||
*/
|
||||
public class OpenaiConfig extends BaseConfig {
|
||||
|
||||
private final String API_URL = "https://api.openai.com/v1";
|
||||
|
||||
private final String DEFAULT_MODEL = Models.Openai.GPT_4O.getModel();
|
||||
|
||||
public OpenaiConfig() {
|
||||
setApiUrl(API_URL);
|
||||
setModel(DEFAULT_MODEL);
|
||||
}
|
||||
|
||||
public OpenaiConfig(String apiKey) {
|
||||
this();
|
||||
setApiKey(apiKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getModelName() {
|
||||
return "openai";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Hutool Team and hutool.cn
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package cn.hutool.ai.model.openai;
|
||||
|
||||
import cn.hutool.ai.core.AIConfig;
|
||||
import cn.hutool.ai.core.AIServiceProvider;
|
||||
|
||||
/**
|
||||
* 创建Openai服务实现类
|
||||
*
|
||||
* @author elichow
|
||||
* @since 5.8.38
|
||||
*/
|
||||
public class OpenaiProvider implements AIServiceProvider {
|
||||
|
||||
@Override
|
||||
public String getServiceName() {
|
||||
return "openai";
|
||||
}
|
||||
|
||||
@Override
|
||||
public OpenaiService create(final AIConfig config) {
|
||||
return new OpenaiServiceImpl(config);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,288 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Hutool Team and hutool.cn
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package cn.hutool.ai.model.openai;
|
||||
|
||||
import cn.hutool.ai.core.AIService;
|
||||
import cn.hutool.ai.core.Message;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* openai支持的扩展接口
|
||||
*
|
||||
* @author elichow
|
||||
* @since 5.8.38
|
||||
*/
|
||||
public interface OpenaiService extends AIService {
|
||||
|
||||
/**
|
||||
* 图像理解:模型会依据传入的图片信息以及问题,给出回复。
|
||||
*
|
||||
* @param prompt 题词
|
||||
* @param images 图片列表/或者图片Base64编码图片列表(URI形式)
|
||||
* @param detail 手动设置图片的质量,取值范围high、low、auto,默认为auto
|
||||
* @return AI回答
|
||||
* @since 5.8.38
|
||||
*/
|
||||
String chatVision(String prompt, final List<String> images, String detail);
|
||||
|
||||
/**
|
||||
* 图像理解-SSE流式输出
|
||||
*
|
||||
* @param prompt 题词
|
||||
* @param images 图片列表/或者图片Base64编码图片列表(URI形式)
|
||||
* @param detail 手动设置图片的质量,取值范围high、low、auto,默认为auto
|
||||
* @param callback 流式数据回调函数
|
||||
* @since 5.8.39
|
||||
*/
|
||||
void chatVision(String prompt, final List<String> images, String detail,final Consumer<String> callback);
|
||||
|
||||
|
||||
/**
|
||||
* 图像理解:模型会依据传入的图片信息以及问题,给出回复。
|
||||
*
|
||||
* @param prompt 题词
|
||||
* @param images 传入的图片列表地址/或者图片Base64编码图片列表(URI形式)
|
||||
* @return AI回答
|
||||
* @since 5.8.38
|
||||
*/
|
||||
default String chatVision(String prompt, final List<String> images) {
|
||||
return chatVision(prompt, images, OpenaiCommon.OpenaiVision.AUTO.getDetail());
|
||||
}
|
||||
|
||||
/**
|
||||
* 图像理解-SSE流式输出
|
||||
*
|
||||
* @param prompt 题词
|
||||
* @param images 传入的图片列表地址/或者图片Base64编码图片列表(URI形式)
|
||||
* @param callback 流式数据回调函数
|
||||
* @since 5.8.39
|
||||
*/
|
||||
default void chatVision(String prompt, final List<String> images, final Consumer<String> callback){
|
||||
chatVision(prompt, images, OpenaiCommon.OpenaiVision.AUTO.getDetail(), callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* 文生图 请设置config中model为支持图片功能的模型 DALL·E系列
|
||||
*
|
||||
* @param prompt 题词
|
||||
* @return 包含生成图片的url
|
||||
* @since 5.8.38
|
||||
*/
|
||||
String imagesGenerations(String prompt);
|
||||
|
||||
/**
|
||||
* 图片编辑 该方法仅支持 DALL·E 2 model
|
||||
*
|
||||
* @param prompt 题词
|
||||
* @param image 需要编辑的图像必须是 PNG 格式
|
||||
* @param mask 如果提供,则是一个与编辑图像大小相同的遮罩图像应该是灰度图,白色表示需要编辑的区域,黑色表示不需要编辑的区域。
|
||||
* @return 包含生成图片的url
|
||||
* @since 5.8.38
|
||||
*/
|
||||
String imagesEdits(String prompt, final File image, final File mask);
|
||||
|
||||
/**
|
||||
* 图片编辑 该方法仅支持 DALL·E 2 model
|
||||
*
|
||||
* @param prompt 题词
|
||||
* @param image 需要编辑的图像必须是 PNG 格式
|
||||
* @return 包含生成图片的url
|
||||
* @since 5.8.38
|
||||
*/
|
||||
default String imagesEdits(String prompt, final File image) {
|
||||
return imagesEdits(prompt, image, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 图片变形 该方法仅支持 DALL·E 2 model
|
||||
*
|
||||
* @param image 需要变形的图像必须是 PNG 格式
|
||||
* @return 包含生成图片的url
|
||||
* @since 5.8.38
|
||||
*/
|
||||
String imagesVariations(final File image);
|
||||
|
||||
/**
|
||||
* TTS文本转语音 请设置config中model为支持TTS功能的模型 TTS系列
|
||||
*
|
||||
* @param input 需要转成语音的文本
|
||||
* @param voice AI的音色
|
||||
* @return 返回的音频mp3文件流
|
||||
* @since 5.8.38
|
||||
*/
|
||||
InputStream textToSpeech(String input, final OpenaiCommon.OpenaiSpeech voice);
|
||||
|
||||
/**
|
||||
* TTS文本转语音 请设置config中model为支持TTS功能的模型 TTS系列
|
||||
*
|
||||
* @param input 需要转成语音的文本
|
||||
* @return 返回的音频mp3文件流
|
||||
* @since 5.8.38
|
||||
*/
|
||||
default InputStream textToSpeech(String input) {
|
||||
return textToSpeech(input, OpenaiCommon.OpenaiSpeech.ALLOY);
|
||||
}
|
||||
|
||||
/**
|
||||
* STT音频转文本 请设置config中model为支持STT功能的模型 whisper
|
||||
*
|
||||
* @param file 需要转成文本的音频文件
|
||||
* @return 返回的文本内容
|
||||
* @since 5.8.38
|
||||
*/
|
||||
String speechToText(final File file);
|
||||
|
||||
/**
|
||||
* 文本向量化 请设置config中model为支持文本向量化功能的模型 text-embedding系列
|
||||
*
|
||||
* @param input 需要向量化的内容
|
||||
* @return 处理后的向量信息
|
||||
* @since 5.8.38
|
||||
*/
|
||||
String embeddingText(String input);
|
||||
|
||||
/**
|
||||
* 检查文本或图像是否具有潜在的危害性
|
||||
* 仅支持omni-moderation-latest和text-moderation-latest模型
|
||||
*
|
||||
* @param text 需要检查的文本
|
||||
* @param imgUrl 需要检查的图片地址
|
||||
* @return AI返回结果
|
||||
* @since 5.8.38
|
||||
*/
|
||||
String moderations(String text, String imgUrl);
|
||||
|
||||
/**
|
||||
* 检查文本是否具有潜在的危害性
|
||||
* 仅支持omni-moderation-latest和text-moderation-latest模型
|
||||
*
|
||||
* @param text 需要检查的文本
|
||||
* @return AI返回结果
|
||||
* @since 5.8.38
|
||||
*/
|
||||
default String moderations(String text) {
|
||||
return moderations(text, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 推理chat
|
||||
* 支持o3-mini和o1
|
||||
*
|
||||
* @param prompt 对话题词
|
||||
* @param reasoningEffort 推理程度
|
||||
* @return AI回答
|
||||
* @since 5.8.38
|
||||
*/
|
||||
default String chatReasoning(String prompt, String reasoningEffort){
|
||||
final List<Message> messages = new ArrayList<>();
|
||||
messages.add(new Message("system", "You are a helpful assistant"));
|
||||
messages.add(new Message("user", prompt));
|
||||
return chatReasoning(messages, reasoningEffort);
|
||||
}
|
||||
|
||||
/**
|
||||
* 推理chat-SSE流式输出
|
||||
* 支持o3-mini和o1
|
||||
*
|
||||
* @param prompt 对话题词
|
||||
* @param reasoningEffort 推理程度
|
||||
* @param callback 流式数据回调函数
|
||||
* @since 5.8.39
|
||||
*/
|
||||
default void chatReasoning(String prompt, String reasoningEffort, final Consumer<String> callback){
|
||||
final List<Message> messages = new ArrayList<>();
|
||||
messages.add(new Message("system", "You are a helpful assistant"));
|
||||
messages.add(new Message("user", prompt));
|
||||
chatReasoning(messages, reasoningEffort, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* 推理chat
|
||||
* 支持o3-mini和o1
|
||||
*
|
||||
* @param prompt 对话题词
|
||||
* @return AI回答
|
||||
* @since 5.8.38
|
||||
*/
|
||||
default String chatReasoning(String prompt) {
|
||||
return chatReasoning(prompt, OpenaiCommon.OpenaiReasoning.MEDIUM.getEffort());
|
||||
}
|
||||
|
||||
/**
|
||||
* 推理chat-SSE流式输出
|
||||
* 支持o3-mini和o1
|
||||
*
|
||||
* @param prompt 对话题词
|
||||
* @param callback 流式数据回调函数
|
||||
* @since 5.8.39
|
||||
*/
|
||||
default void chatReasoning(String prompt, final Consumer<String> callback) {
|
||||
chatReasoning(prompt, OpenaiCommon.OpenaiReasoning.MEDIUM.getEffort(), callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* 推理chat
|
||||
* 支持o3-mini和o1
|
||||
*
|
||||
* @param messages 消息列表
|
||||
* @param reasoningEffort 推理程度
|
||||
* @return AI回答
|
||||
* @since 5.8.38
|
||||
*/
|
||||
String chatReasoning(final List<Message> messages, String reasoningEffort);
|
||||
|
||||
/**
|
||||
* 推理chat-SSE流式输出
|
||||
* 支持o3-mini和o1
|
||||
*
|
||||
* @param messages 消息列表
|
||||
* @param reasoningEffort 推理程度
|
||||
* @param callback 流式数据回调函数
|
||||
* @since 5.8.39
|
||||
*/
|
||||
void chatReasoning(final List<Message> messages, String reasoningEffort, final Consumer<String> callback);
|
||||
|
||||
/**
|
||||
* 推理chat
|
||||
* 支持o3-mini和o1
|
||||
*
|
||||
* @param messages 消息列表
|
||||
* @return AI回答
|
||||
* @since 5.8.38
|
||||
*/
|
||||
default String chatReasoning(final List<Message> messages) {
|
||||
return chatReasoning(messages, OpenaiCommon.OpenaiReasoning.MEDIUM.getEffort());
|
||||
}
|
||||
|
||||
/**
|
||||
* 推理chat-SSE流式输出
|
||||
* 支持o3-mini和o1
|
||||
*
|
||||
* @param messages 消息列表
|
||||
* @param callback 流式数据回调函数
|
||||
* @since 5.8.39
|
||||
*/
|
||||
default void chatReasoning(final List<Message> messages, final Consumer<String> callback) {
|
||||
chatReasoning(messages, OpenaiCommon.OpenaiReasoning.MEDIUM.getEffort(), callback);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,365 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Hutool Team and hutool.cn
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package cn.hutool.ai.model.openai;
|
||||
|
||||
import cn.hutool.ai.core.AIConfig;
|
||||
import cn.hutool.ai.core.BaseAIService;
|
||||
import cn.hutool.ai.core.Message;
|
||||
import cn.hutool.core.thread.ThreadUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.http.HttpResponse;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* openai服务,AI具体功能的实现
|
||||
*
|
||||
* @author elichow
|
||||
* @since 5.8.38
|
||||
*/
|
||||
public class OpenaiServiceImpl extends BaseAIService implements OpenaiService {
|
||||
|
||||
//对话
|
||||
private final String CHAT_ENDPOINT = "/chat/completions";
|
||||
//文生图
|
||||
private final String IMAGES_GENERATIONS = "/images/generations";
|
||||
//图片编辑
|
||||
private final String IMAGES_EDITS = "/images/edits";
|
||||
//图片变形
|
||||
private final String IMAGES_VARIATIONS = "/images/variations";
|
||||
//文本转语音
|
||||
private final String TTS = "/audio/speech";
|
||||
//语音转文本
|
||||
private final String STT = "/audio/transcriptions";
|
||||
//文本向量化
|
||||
private final String EMBEDDINGS = "/embeddings";
|
||||
//检查文本或图片
|
||||
private final String MODERATIONS = "/moderations";
|
||||
|
||||
public OpenaiServiceImpl(final AIConfig config) {
|
||||
//初始化Openai客户端
|
||||
super(config);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String chat(final List<Message> messages) {
|
||||
String paramJson = buildChatRequestBody(messages);
|
||||
final HttpResponse response = sendPost(CHAT_ENDPOINT, paramJson);
|
||||
return response.body();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void chat(List<Message> messages,Consumer<String> callback) {
|
||||
Map<String, Object> paramMap = buildChatStreamRequestBody(messages);
|
||||
ThreadUtil.newThread(() -> sendPostStream(CHAT_ENDPOINT, paramMap, callback::accept), "openai-chat-sse").start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String chatVision(String prompt, final List<String> images, String detail) {
|
||||
String paramJson = buildChatVisionRequestBody(prompt, images, detail);
|
||||
final HttpResponse response = sendPost(CHAT_ENDPOINT, paramJson);
|
||||
return response.body();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void chatVision(String prompt, List<String> images, String detail, Consumer<String> callback) {
|
||||
Map<String, Object> paramMap = buildChatVisionStreamRequestBody(prompt, images, detail);
|
||||
ThreadUtil.newThread(() -> sendPostStream(CHAT_ENDPOINT, paramMap, callback::accept), "openai-chatVision-sse").start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String imagesGenerations(String prompt) {
|
||||
String paramJson = buildImagesGenerationsRequestBody(prompt);
|
||||
final HttpResponse response = sendPost(IMAGES_GENERATIONS, paramJson);
|
||||
return response.body();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String imagesEdits(String prompt, final File image, final File mask) {
|
||||
final Map<String, Object> paramMap = buildImagesEditsRequestBody(prompt, image, mask);
|
||||
final HttpResponse response = sendFormData(IMAGES_EDITS, paramMap);
|
||||
return response.body();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String imagesVariations(final File image) {
|
||||
final Map<String, Object> paramMap = buildImagesVariationsRequestBody(image);
|
||||
final HttpResponse response = sendFormData(IMAGES_VARIATIONS, paramMap);
|
||||
return response.body();
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream textToSpeech(String input, final OpenaiCommon.OpenaiSpeech voice) {
|
||||
String paramJson = buildTTSRequestBody(input, voice.getVoice());
|
||||
final HttpResponse response = sendPost(TTS, paramJson);
|
||||
return response.bodyStream();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String speechToText(final File file) {
|
||||
final Map<String, Object> paramMap = buildSTTRequestBody(file);
|
||||
final HttpResponse response = sendFormData(STT, paramMap);
|
||||
return response.body();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String embeddingText(String input) {
|
||||
String paramJson = buildEmbeddingTextRequestBody(input);
|
||||
final HttpResponse response = sendPost(EMBEDDINGS, paramJson);
|
||||
return response.body();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String moderations(String text, String imgUrl) {
|
||||
String paramJson = buileModerationsRequestBody(text, imgUrl);
|
||||
final HttpResponse response = sendPost(MODERATIONS, paramJson);
|
||||
return response.body();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String chatReasoning(final List<Message> messages, String reasoningEffort) {
|
||||
String paramJson = buildChatReasoningRequestBody(messages, reasoningEffort);
|
||||
final HttpResponse response = sendPost(CHAT_ENDPOINT, paramJson);
|
||||
return response.body();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void chatReasoning(List<Message> messages, String reasoningEffort, Consumer<String> callback) {
|
||||
Map<String, Object> paramMap = buildChatReasoningStreamRequestBody(messages, reasoningEffort);
|
||||
ThreadUtil.newThread(() -> sendPostStream(CHAT_ENDPOINT, paramMap, callback::accept), "openai-chatReasoning-sse").start();
|
||||
}
|
||||
|
||||
// 构建chat请求体
|
||||
private String buildChatRequestBody(final List<Message> messages) {
|
||||
//使用JSON工具
|
||||
final Map<String, Object> paramMap = new HashMap<>();
|
||||
paramMap.put("model", config.getModel());
|
||||
paramMap.put("messages", messages);
|
||||
//合并其他参数
|
||||
paramMap.putAll(config.getAdditionalConfigMap());
|
||||
|
||||
return JSONUtil.toJsonStr(paramMap);
|
||||
}
|
||||
|
||||
private Map<String, Object> buildChatStreamRequestBody(final List<Message> messages) {
|
||||
//使用JSON工具
|
||||
final Map<String, Object> paramMap = new HashMap<>();
|
||||
paramMap.put("stream", true);
|
||||
paramMap.put("model", config.getModel());
|
||||
paramMap.put("messages", messages);
|
||||
//合并其他参数
|
||||
paramMap.putAll(config.getAdditionalConfigMap());
|
||||
|
||||
return paramMap;
|
||||
}
|
||||
|
||||
//构建chatVision请求体
|
||||
private String buildChatVisionRequestBody(String prompt, final List<String> images, String detail) {
|
||||
// 定义消息结构
|
||||
final List<Message> messages = new ArrayList<>();
|
||||
final List<Object> content = new ArrayList<>();
|
||||
|
||||
final Map<String, String> contentMap = new HashMap<>();
|
||||
contentMap.put("type", "text");
|
||||
contentMap.put("text", prompt);
|
||||
content.add(contentMap);
|
||||
for (String img : images) {
|
||||
final Map<String, Object> imgUrlMap = new HashMap<>();
|
||||
imgUrlMap.put("type", "image_url");
|
||||
final Map<String, String> urlMap = new HashMap<>();
|
||||
urlMap.put("url", img);
|
||||
urlMap.put("detail", detail);
|
||||
imgUrlMap.put("image_url", urlMap);
|
||||
content.add(imgUrlMap);
|
||||
}
|
||||
|
||||
messages.add(new Message("user", content));
|
||||
|
||||
//使用JSON工具
|
||||
final Map<String, Object> paramMap = new HashMap<>();
|
||||
paramMap.put("model", config.getModel());
|
||||
paramMap.put("messages", messages);
|
||||
//合并其他参数
|
||||
paramMap.putAll(config.getAdditionalConfigMap());
|
||||
return JSONUtil.toJsonStr(paramMap);
|
||||
}
|
||||
|
||||
private Map<String, Object> buildChatVisionStreamRequestBody(String prompt, final List<String> images, String detail) {
|
||||
// 定义消息结构
|
||||
final List<Message> messages = new ArrayList<>();
|
||||
final List<Object> content = new ArrayList<>();
|
||||
|
||||
final Map<String, String> contentMap = new HashMap<>();
|
||||
contentMap.put("type", "text");
|
||||
contentMap.put("text", prompt);
|
||||
content.add(contentMap);
|
||||
for (String img : images) {
|
||||
HashMap<String, Object> imgUrlMap = new HashMap<>();
|
||||
imgUrlMap.put("type", "image_url");
|
||||
HashMap<String, String> urlMap = new HashMap<>();
|
||||
urlMap.put("url", img);
|
||||
urlMap.put("detail", detail);
|
||||
imgUrlMap.put("image_url", urlMap);
|
||||
content.add(imgUrlMap);
|
||||
}
|
||||
|
||||
messages.add(new Message("user", content));
|
||||
|
||||
//使用JSON工具
|
||||
final Map<String, Object> paramMap = new HashMap<>();
|
||||
paramMap.put("stream", true);
|
||||
paramMap.put("model", config.getModel());
|
||||
paramMap.put("messages", messages);
|
||||
//合并其他参数
|
||||
paramMap.putAll(config.getAdditionalConfigMap());
|
||||
return paramMap;
|
||||
}
|
||||
|
||||
//构建文生图请求体
|
||||
private String buildImagesGenerationsRequestBody(String prompt) {
|
||||
final Map<String, Object> paramMap = new HashMap<>();
|
||||
paramMap.put("model", config.getModel());
|
||||
paramMap.put("prompt", prompt);
|
||||
//合并其他参数
|
||||
paramMap.putAll(config.getAdditionalConfigMap());
|
||||
|
||||
return JSONUtil.toJsonStr(paramMap);
|
||||
}
|
||||
|
||||
//构建图片编辑请求体
|
||||
private Map<String, Object> buildImagesEditsRequestBody(String prompt, final File image, final File mask) {
|
||||
final Map<String, Object> paramMap = new HashMap<>();
|
||||
paramMap.put("model", config.getModel());
|
||||
paramMap.put("prompt", prompt);
|
||||
paramMap.put("image", image);
|
||||
if (mask != null) {
|
||||
paramMap.put("mask", mask);
|
||||
}
|
||||
//合并其他参数
|
||||
paramMap.putAll(config.getAdditionalConfigMap());
|
||||
|
||||
return paramMap;
|
||||
}
|
||||
|
||||
//构建图片变形请求体
|
||||
private Map<String, Object> buildImagesVariationsRequestBody(final File image) {
|
||||
final Map<String, Object> paramMap = new HashMap<>();
|
||||
paramMap.put("model", config.getModel());
|
||||
paramMap.put("image", image);
|
||||
//合并其他参数
|
||||
paramMap.putAll(config.getAdditionalConfigMap());
|
||||
|
||||
return paramMap;
|
||||
}
|
||||
|
||||
//构建TTS请求体
|
||||
private String buildTTSRequestBody(String input, String voice) {
|
||||
final Map<String, Object> paramMap = new HashMap<>();
|
||||
paramMap.put("model", config.getModel());
|
||||
paramMap.put("input", input);
|
||||
paramMap.put("voice", voice);
|
||||
//合并其他参数
|
||||
paramMap.putAll(config.getAdditionalConfigMap());
|
||||
|
||||
return JSONUtil.toJsonStr(paramMap);
|
||||
}
|
||||
|
||||
//构建STT请求体
|
||||
private Map<String, Object> buildSTTRequestBody(final File file) {
|
||||
final Map<String, Object> paramMap = new HashMap<>();
|
||||
paramMap.put("model", config.getModel());
|
||||
paramMap.put("file", file);
|
||||
//合并其他参数
|
||||
paramMap.putAll(config.getAdditionalConfigMap());
|
||||
|
||||
return paramMap;
|
||||
}
|
||||
|
||||
//构建文本向量化请求体
|
||||
private String buildEmbeddingTextRequestBody(String input) {
|
||||
//使用JSON工具
|
||||
final Map<String, Object> paramMap = new HashMap<>();
|
||||
paramMap.put("model", config.getModel());
|
||||
paramMap.put("input", input);
|
||||
//合并其他参数
|
||||
paramMap.putAll(config.getAdditionalConfigMap());
|
||||
return JSONUtil.toJsonStr(paramMap);
|
||||
}
|
||||
|
||||
//构建检查图片或文字请求体
|
||||
private String buileModerationsRequestBody(String text, String imgUrl) {
|
||||
//使用JSON工具
|
||||
final Map<String, Object> paramMap = new HashMap<>();
|
||||
paramMap.put("model", config.getModel());
|
||||
|
||||
final List<Object> input = new ArrayList<>();
|
||||
//添加文本参数
|
||||
if (!StrUtil.isBlank(text)) {
|
||||
final Map<String, String> textMap = new HashMap<>();
|
||||
textMap.put("type", "text");
|
||||
textMap.put("text", text);
|
||||
input.add(textMap);
|
||||
}
|
||||
//添加图片参数
|
||||
if (!StrUtil.isBlank(imgUrl)) {
|
||||
final Map<String, Object> imgUrlMap = new HashMap<>();
|
||||
imgUrlMap.put("type", "image_url");
|
||||
final Map<String, String> urlMap = new HashMap<>();
|
||||
urlMap.put("url", imgUrl);
|
||||
imgUrlMap.put("image_url", urlMap);
|
||||
input.add(imgUrlMap);
|
||||
}
|
||||
|
||||
paramMap.put("input", input);
|
||||
//合并其他参数
|
||||
paramMap.putAll(config.getAdditionalConfigMap());
|
||||
|
||||
return JSONUtil.toJsonStr(paramMap);
|
||||
}
|
||||
|
||||
//构建推理请求体
|
||||
private String buildChatReasoningRequestBody(final List<Message> messages, String reasoningEffort) {
|
||||
final Map<String, Object> paramMap = new HashMap<>();
|
||||
paramMap.put("model", config.getModel());
|
||||
paramMap.put("messages", messages);
|
||||
paramMap.put("reasoning_effort", reasoningEffort);
|
||||
//合并其他参数
|
||||
paramMap.putAll(config.getAdditionalConfigMap());
|
||||
|
||||
return JSONUtil.toJsonStr(paramMap);
|
||||
}
|
||||
|
||||
private Map<String, Object> buildChatReasoningStreamRequestBody(final List<Message> messages, String reasoningEffort) {
|
||||
final Map<String, Object> paramMap = new HashMap<>();
|
||||
paramMap.put("stream", true);
|
||||
paramMap.put("model", config.getModel());
|
||||
paramMap.put("messages", messages);
|
||||
paramMap.put("reasoning_effort", reasoningEffort);
|
||||
//合并其他参数
|
||||
paramMap.putAll(config.getAdditionalConfigMap());
|
||||
|
||||
return paramMap;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Hutool Team and hutool.cn
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* 对openai的封装实现
|
||||
*
|
||||
* @author elichow
|
||||
* @since 5.8.38
|
||||
*/
|
||||
|
||||
package cn.hutool.ai.model.openai;
|
||||
24
hutool-ai/src/main/java/cn/hutool/ai/model/package-info.java
Normal file
24
hutool-ai/src/main/java/cn/hutool/ai/model/package-info.java
Normal file
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Hutool Team and hutool.cn
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* 对各个AI大模型的相关封装
|
||||
*
|
||||
* @author elichow
|
||||
* @since 5.8.38
|
||||
*/
|
||||
|
||||
package cn.hutool.ai.model;
|
||||
24
hutool-ai/src/main/java/cn/hutool/ai/package-info.java
Normal file
24
hutool-ai/src/main/java/cn/hutool/ai/package-info.java
Normal file
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Hutool Team and hutool.cn
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Hutool-ai主要用于AI大模型的封装,只需要对AI模型最基本的设置,即可调用AI大模型。
|
||||
*
|
||||
* @author elichow
|
||||
* @since 5.8.38
|
||||
*/
|
||||
|
||||
package cn.hutool.ai;
|
||||
@@ -0,0 +1,5 @@
|
||||
cn.hutool.ai.model.hutool.HutoolConfig
|
||||
cn.hutool.ai.model.deepseek.DeepSeekConfig
|
||||
cn.hutool.ai.model.openai.OpenaiConfig
|
||||
cn.hutool.ai.model.doubao.DoubaoConfig
|
||||
cn.hutool.ai.model.grok.GrokConfig
|
||||
@@ -0,0 +1,5 @@
|
||||
cn.hutool.ai.model.hutool.HutoolProvider
|
||||
cn.hutool.ai.model.deepseek.DeepSeekProvider
|
||||
cn.hutool.ai.model.openai.OpenaiProvider
|
||||
cn.hutool.ai.model.doubao.DoubaoProvider
|
||||
cn.hutool.ai.model.grok.GrokProvider
|
||||
41
hutool-ai/src/test/java/AIServiceFactoryTest.java
Normal file
41
hutool-ai/src/test/java/AIServiceFactoryTest.java
Normal file
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Hutool Team and hutool.cn
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import cn.hutool.ai.AIServiceFactory;
|
||||
import cn.hutool.ai.ModelName;
|
||||
import cn.hutool.ai.core.AIConfigBuilder;
|
||||
import cn.hutool.ai.core.AIService;
|
||||
import cn.hutool.ai.model.deepseek.DeepSeekService;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
|
||||
class AIServiceFactoryTest {
|
||||
|
||||
String key = "your key";
|
||||
|
||||
@Test
|
||||
void getAIService() {
|
||||
final AIService aiService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.DEEPSEEK.getValue()).setApiKey(key).build());
|
||||
assertNotNull(aiService);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetAIService() {
|
||||
final DeepSeekService deepSeekService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.DEEPSEEK.getValue()).setApiKey(key).build(), DeepSeekService.class);
|
||||
assertNotNull(deepSeekService);
|
||||
}
|
||||
}
|
||||
94
hutool-ai/src/test/java/AIUtilTest.java
Normal file
94
hutool-ai/src/test/java/AIUtilTest.java
Normal file
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Hutool Team and hutool.cn
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import cn.hutool.ai.AIUtil;
|
||||
import cn.hutool.ai.ModelName;
|
||||
import cn.hutool.ai.core.AIConfigBuilder;
|
||||
import cn.hutool.ai.core.AIService;
|
||||
import cn.hutool.ai.core.Message;
|
||||
import cn.hutool.ai.model.deepseek.DeepSeekService;
|
||||
import cn.hutool.ai.model.doubao.DoubaoService;
|
||||
import cn.hutool.ai.model.grok.GrokService;
|
||||
import cn.hutool.ai.model.hutool.HutoolService;
|
||||
import cn.hutool.ai.model.openai.OpenaiService;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
|
||||
class AIUtilTest {
|
||||
|
||||
String key = "your key";
|
||||
|
||||
@Test
|
||||
void getAIService() {
|
||||
final DeepSeekService deepSeekService = AIUtil.getAIService(new AIConfigBuilder(ModelName.DEEPSEEK.getValue()).setApiKey(key).build(), DeepSeekService.class);
|
||||
assertNotNull(deepSeekService);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetAIService() {
|
||||
final AIService aiService = AIUtil.getAIService(new AIConfigBuilder(ModelName.OPENAI.getValue()).setApiKey(key).build());
|
||||
assertNotNull(aiService);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getHutoolService() {
|
||||
final HutoolService hutoolService = AIUtil.getHutoolService(new AIConfigBuilder(ModelName.HUTOOL.getValue()).setApiKey(key).build());
|
||||
assertNotNull(hutoolService);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getDeepSeekService() {
|
||||
final DeepSeekService deepSeekService = AIUtil.getDeepSeekService(new AIConfigBuilder(ModelName.DEEPSEEK.getValue()).setApiKey(key).build());
|
||||
assertNotNull(deepSeekService);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getDoubaoService() {
|
||||
final DoubaoService doubaoService = AIUtil.getDoubaoService(new AIConfigBuilder(ModelName.DOUBAO.getValue()).setApiKey(key).build());
|
||||
assertNotNull(doubaoService);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getGrokService() {
|
||||
final GrokService grokService = AIUtil.getGrokService(new AIConfigBuilder(ModelName.GROK.getValue()).setApiKey(key).build());
|
||||
assertNotNull(grokService);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getOpenAIService() {
|
||||
final OpenaiService openAIService = AIUtil.getOpenAIService(new AIConfigBuilder(ModelName.OPENAI.getValue()).setApiKey(key).build());
|
||||
assertNotNull(openAIService);
|
||||
}
|
||||
|
||||
@Test
|
||||
void chat() {
|
||||
final String chat = AIUtil.chat(new AIConfigBuilder(ModelName.DEEPSEEK.getValue()).setApiKey(key).build(), "写一首赞美我的诗");
|
||||
assertNotNull(chat);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testChat() {
|
||||
final List<Message> messages = new ArrayList<>();
|
||||
messages.add(new Message("system","你是财神爷,只会说“我是财神”"));
|
||||
messages.add(new Message("user","你是谁啊?"));
|
||||
final String chat = AIUtil.chat(new AIConfigBuilder(ModelName.DEEPSEEK.getValue()).setApiKey(key).build(), messages);
|
||||
assertNotNull(chat);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Hutool Team and hutool.cn
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package cn.hutool.ai.model.deepseek;
|
||||
|
||||
import cn.hutool.ai.AIServiceFactory;
|
||||
import cn.hutool.ai.ModelName;
|
||||
import cn.hutool.ai.core.AIConfigBuilder;
|
||||
import cn.hutool.ai.core.Message;
|
||||
import cn.hutool.core.thread.ThreadUtil;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
|
||||
class DeepSeekServiceTest {
|
||||
|
||||
String key = "your key";
|
||||
DeepSeekService deepSeekService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.DEEPSEEK.getValue()).setApiKey(key).build(),DeepSeekService.class);
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void chat(){
|
||||
final String chat = deepSeekService.chat("写一个疯狂星期四广告词");
|
||||
assertNotNull(chat);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void chatStream() {
|
||||
String prompt = "写一个疯狂星期四广告词";
|
||||
// 使用AtomicBoolean作为结束标志
|
||||
AtomicBoolean isDone = new AtomicBoolean(false);
|
||||
|
||||
deepSeekService.chat(prompt, data -> {
|
||||
assertNotNull(data);
|
||||
if (data.contains("[DONE]")) {
|
||||
// 设置结束标志
|
||||
isDone.set(true);
|
||||
} else if (data.contains("\"error\"")) {
|
||||
isDone.set(true);
|
||||
}
|
||||
|
||||
});
|
||||
// 轮询检查结束标志
|
||||
while (!isDone.get()) {
|
||||
ThreadUtil.sleep(100);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void testChat(){
|
||||
final List<Message> messages = new ArrayList<>();
|
||||
messages.add(new Message("system","你是个抽象大师,会说很抽象的话,最擅长说抽象的笑话"));
|
||||
messages.add(new Message("user","给我说一个笑话"));
|
||||
final String chat = deepSeekService.chat(messages);
|
||||
assertNotNull(chat);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void beta() {
|
||||
final String beta = deepSeekService.beta("写一个疯狂星期四广告词");
|
||||
assertNotNull(beta);
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void betaStream() {
|
||||
String beta = "写一个疯狂星期四广告词";
|
||||
// 使用AtomicBoolean作为结束标志
|
||||
AtomicBoolean isDone = new AtomicBoolean(false);
|
||||
|
||||
deepSeekService.beta(beta, data -> {
|
||||
assertNotNull(data);
|
||||
if (data.contains("[DONE]")) {
|
||||
// 设置结束标志
|
||||
isDone.set(true);
|
||||
} else if (data.contains("\"error\"")) {
|
||||
isDone.set(true);
|
||||
}
|
||||
|
||||
});
|
||||
// 轮询检查结束标志
|
||||
while (!isDone.get()) {
|
||||
ThreadUtil.sleep(100);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void models() {
|
||||
final String models = deepSeekService.models();
|
||||
assertNotNull(models);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void balance() {
|
||||
final String balance = deepSeekService.balance();
|
||||
assertNotNull(balance);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,311 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Hutool Team and hutool.cn
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package cn.hutool.ai.model.doubao;
|
||||
|
||||
import cn.hutool.ai.AIServiceFactory;
|
||||
import cn.hutool.ai.ModelName;
|
||||
import cn.hutool.ai.Models;
|
||||
import cn.hutool.ai.core.AIConfigBuilder;
|
||||
import cn.hutool.ai.core.Message;
|
||||
import cn.hutool.core.img.ImgUtil;
|
||||
import cn.hutool.core.thread.ThreadUtil;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.awt.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
|
||||
class DoubaoServiceTest {
|
||||
|
||||
String key = "your key";
|
||||
DoubaoService doubaoService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.DOUBAO.getValue()).setModel(Models.Doubao.DOUBAO_1_5_LITE_32K.getModel()).setApiKey(key).build(), DoubaoService.class);
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void chat(){
|
||||
final String chat = doubaoService.chat("写一个疯狂星期四广告词");
|
||||
assertNotNull(chat);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void chatStream() {
|
||||
String prompt = "写一个疯狂星期四广告词";
|
||||
// 使用AtomicBoolean作为结束标志
|
||||
AtomicBoolean isDone = new AtomicBoolean(false);
|
||||
|
||||
doubaoService.chat(prompt, data -> {
|
||||
assertNotNull(data);
|
||||
if (data.contains("[DONE]")) {
|
||||
// 设置结束标志
|
||||
isDone.set(true);
|
||||
} else if (data.contains("\"error\"")) {
|
||||
isDone.set(true);
|
||||
}
|
||||
|
||||
});
|
||||
// 轮询检查结束标志
|
||||
while (!isDone.get()) {
|
||||
ThreadUtil.sleep(100);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void testChat(){
|
||||
final List<Message> messages = new ArrayList<>();
|
||||
messages.add(new Message("system","你是个抽象大师,会说很抽象的话,最擅长说抽象的笑话"));
|
||||
messages.add(new Message("user","给我说一个笑话"));
|
||||
final String chat = doubaoService.chat(messages);
|
||||
assertNotNull(chat);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void chatVision() {
|
||||
final DoubaoService doubaoService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.DOUBAO.getValue())
|
||||
.setApiKey(key).setModel(Models.Doubao.DOUBAO_1_5_VISION_PRO_32K.getModel()).build(), DoubaoService.class);
|
||||
final String base64 = ImgUtil.toBase64DataUri(Toolkit.getDefaultToolkit().createImage("your imageUrl"), "png");
|
||||
final String chatVision = doubaoService.chatVision("图片上有些什么?", Arrays.asList(base64));
|
||||
assertNotNull(chatVision);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void testChatVision() {
|
||||
final DoubaoService doubaoService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.DOUBAO.getValue())
|
||||
.setApiKey(key).setModel(Models.Doubao.DOUBAO_1_5_VISION_PRO_32K.getModel()).build(), DoubaoService.class);
|
||||
final String chatVision = doubaoService.chatVision("图片上有些什么?", Arrays.asList("https://img2.baidu.com/it/u=862000265,4064861820&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=1544"),DoubaoCommon.DoubaoVision.HIGH.getDetail());
|
||||
assertNotNull(chatVision);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void testChatVisionStream() {
|
||||
final DoubaoService doubaoService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.DOUBAO.getValue())
|
||||
.setApiKey(key).setModel(Models.Doubao.DOUBAO_1_5_VISION_PRO_32K.getModel()).build(), DoubaoService.class);
|
||||
|
||||
String prompt = "图片上有些什么?";
|
||||
List<String> images = Arrays.asList("https://img2.baidu.com/it/u=862000265,4064861820&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=1544");
|
||||
|
||||
// 使用AtomicBoolean作为结束标志
|
||||
AtomicBoolean isDone = new AtomicBoolean(false);
|
||||
doubaoService.chatVision(prompt,images, data -> {
|
||||
assertNotNull(data);
|
||||
if (data.contains("[DONE]")) {
|
||||
// 设置结束标志
|
||||
isDone.set(true);
|
||||
} else if (data.contains("\"error\"")) {
|
||||
isDone.set(true);
|
||||
}
|
||||
|
||||
});
|
||||
// 轮询检查结束标志
|
||||
while (!isDone.get()) {
|
||||
ThreadUtil.sleep(100);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void videoTasks() {
|
||||
final DoubaoService doubaoService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.DOUBAO.getValue())
|
||||
.setApiKey(key).setModel(Models.Doubao.Doubao_Seedance_1_0_lite_i2v.getModel()).build(), DoubaoService.class);
|
||||
final String videoTasks = doubaoService.videoTasks("生成一段动画视频,主角是大耳朵图图,一个活泼可爱的小男孩。视频中图图在公园里玩耍," +
|
||||
"画面采用明亮温暖的卡通风格,色彩鲜艳,动作流畅。背景音乐轻快活泼,带有冒险感,音效包括鸟叫声、欢笑声和山洞回声。", "https://img2.baidu.com/it/u=862000265,4064861820&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=1544");
|
||||
assertNotNull(videoTasks);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void getVideoTasksInfo() {
|
||||
//cgt-20250306170051-6r9gk
|
||||
final DoubaoService doubaoService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.DOUBAO.getValue())
|
||||
.setApiKey(key).build(), DoubaoService.class);
|
||||
final String videoTasksInfo = doubaoService.getVideoTasksInfo("cgt-20250306170051-6r9gk");
|
||||
assertNotNull(videoTasksInfo);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void embeddingText() {
|
||||
final DoubaoService doubaoService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.DOUBAO.getValue())
|
||||
.setApiKey(key).setModel(Models.Doubao.DOUBAO_EMBEDDING_TEXT_240715.getModel()).build(), DoubaoService.class);
|
||||
final String embeddingText = doubaoService.embeddingText(new String[]{"阿斯顿", "马丁"});
|
||||
assertNotNull(embeddingText);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void embeddingVision() {
|
||||
final DoubaoService doubaoService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.DOUBAO.getValue())
|
||||
.setApiKey(key).setModel(Models.Doubao.DOUBAO_EMBEDDING_VISION.getModel()).build(), DoubaoService.class);
|
||||
final String embeddingVision = doubaoService.embeddingVision("天空好难", "https://img2.baidu.com/it/u=862000265,4064861820&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=1544");
|
||||
assertNotNull(embeddingVision);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void botsChat() {
|
||||
final DoubaoService doubaoService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.DOUBAO.getValue())
|
||||
.setApiKey(key).setModel("your bots id").build(), DoubaoService.class);
|
||||
final ArrayList<Message> messages = new ArrayList<>();
|
||||
messages.add(new Message("system","你是什么都可以"));
|
||||
messages.add(new Message("user","你想做些什么"));
|
||||
final String botsChat = doubaoService.botsChat(messages);
|
||||
assertNotNull(botsChat);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void botsChatStream() {
|
||||
final DoubaoService doubaoService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.DOUBAO.getValue())
|
||||
.setApiKey(key).setModel("your bots id").build(), DoubaoService.class);
|
||||
final ArrayList<Message> messages = new ArrayList<>();
|
||||
messages.add(new Message("system","你是什么都可以"));
|
||||
messages.add(new Message("user","你想做些什么"));
|
||||
|
||||
// 使用AtomicBoolean作为结束标志
|
||||
AtomicBoolean isDone = new AtomicBoolean(false);
|
||||
doubaoService.botsChat(messages, data -> {
|
||||
assertNotNull(data);
|
||||
if (data.contains("[DONE]")) {
|
||||
// 设置结束标志
|
||||
isDone.set(true);
|
||||
} else if (data.contains("\"error\"")) {
|
||||
isDone.set(true);
|
||||
}
|
||||
|
||||
});
|
||||
// 轮询检查结束标志
|
||||
while (!isDone.get()) {
|
||||
ThreadUtil.sleep(100);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void tokenization() {
|
||||
final String tokenization = doubaoService.tokenization(new String[]{"阿斯顿", "马丁"});
|
||||
assertNotNull(tokenization);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void batchChat() {
|
||||
final DoubaoService doubaoService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.DOUBAO.getValue())
|
||||
.setApiKey(key).setModel("your Endpoint ID").build(), DoubaoService.class);
|
||||
final String batchChat = doubaoService.batchChat("写首歌词");
|
||||
assertNotNull(batchChat);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void testBatchChat() {
|
||||
final DoubaoService doubaoService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.DOUBAO.getValue())
|
||||
.setApiKey(key).setModel("your Endpoint ID").build(), DoubaoService.class);
|
||||
final List<Message> messages = new ArrayList<>();
|
||||
messages.add(new Message("system","你是个抽象大师"));
|
||||
messages.add(new Message("user","写一个KFC的抽象广告"));
|
||||
final String batchChat = doubaoService.batchChat(messages);
|
||||
assertNotNull(batchChat);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void createContext() {
|
||||
final DoubaoService doubaoService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.DOUBAO.getValue())
|
||||
.setApiKey(key).setModel("your Endpoint ID").build(), DoubaoService.class);
|
||||
final List<Message> messages = new ArrayList<>();
|
||||
messages.add(new Message("system","你是个抽象大师,你真的很抽象"));
|
||||
final String context = doubaoService.createContext(messages);//ctx-20250307092153-cvslm
|
||||
assertNotNull(context);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void testCreateContext() {
|
||||
final DoubaoService doubaoService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.DOUBAO.getValue())
|
||||
.setApiKey(key).setModel("ep-20250305100610-bvbpc").build(), DoubaoService.class);
|
||||
final List<Message> messages = new ArrayList<>();
|
||||
messages.add(new Message("system","你是个抽象大师,你真的很抽象"));
|
||||
final String context = doubaoService.createContext(messages,DoubaoCommon.DoubaoContext.COMMON_PREFIX.getMode());
|
||||
assertNotNull(context);//ctx-20250307092153-cvslm
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void chatContext() {
|
||||
//ctx-20250307092153-cvslm
|
||||
final DoubaoService doubaoService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.DOUBAO.getValue())
|
||||
.setApiKey(key).setModel("your Endpoint ID").build(), DoubaoService.class);
|
||||
final String chatContext = doubaoService.chatContext("你是谁?", "your contextId");
|
||||
assertNotNull(chatContext);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void testChatContext() {
|
||||
final DoubaoService doubaoService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.DOUBAO.getValue())
|
||||
.setApiKey(key).setModel("your Endpoint ID").build(), DoubaoService.class);
|
||||
final List<Message> messages = new ArrayList<>();
|
||||
messages.add(new Message("user","你怎么看待意大利面拌水泥?"));
|
||||
final String chatContext = doubaoService.chatContext(messages, "your contextId");
|
||||
assertNotNull(chatContext);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void testChatContextStream() {
|
||||
final DoubaoService doubaoService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.DOUBAO.getValue())
|
||||
.setApiKey(key).setModel("your Endpoint ID").build(), DoubaoService.class);
|
||||
final List<Message> messages = new ArrayList<>();
|
||||
messages.add(new Message("user","你怎么看待意大利面拌水泥?"));
|
||||
String contextId = "your contextId";
|
||||
|
||||
// 使用AtomicBoolean作为结束标志
|
||||
AtomicBoolean isDone = new AtomicBoolean(false);
|
||||
doubaoService.chatContext(messages,contextId, data -> {
|
||||
assertNotNull(data);
|
||||
if (data.contains("[DONE]")) {
|
||||
// 设置结束标志
|
||||
isDone.set(true);
|
||||
} else if (data.contains("\"error\"")) {
|
||||
isDone.set(true);
|
||||
}
|
||||
|
||||
});
|
||||
// 轮询检查结束标志
|
||||
while (!isDone.get()) {
|
||||
ThreadUtil.sleep(100);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void imagesGenerations() {
|
||||
final DoubaoService doubaoService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.DOUBAO.getValue())
|
||||
.setApiKey(key).setModel(Models.Doubao.DOUBAO_SEEDREAM_3_0_T2I.getModel()).build(), DoubaoService.class);
|
||||
final String imagesGenerations = doubaoService.imagesGenerations("一位年轻的宇航员站在未来感十足的太空站内,透过巨大的弧形落地窗凝望浩瀚宇宙。窗外,璀璨的星河与五彩斑斓的星云交织,远处隐约可见未知星球的轮廓,仿佛在召唤着探索的脚步。宇航服上的呼吸灯与透明显示屏上的星图交相辉映,象征着人类科技与宇宙奥秘的碰撞。画面深邃而神秘,充满对未知的渴望与无限可能的想象。");
|
||||
assertNotNull(imagesGenerations);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Hutool Team and hutool.cn
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package cn.hutool.ai.model.grok;
|
||||
|
||||
import cn.hutool.ai.AIServiceFactory;
|
||||
import cn.hutool.ai.ModelName;
|
||||
import cn.hutool.ai.Models;
|
||||
import cn.hutool.ai.core.AIConfigBuilder;
|
||||
import cn.hutool.ai.core.Message;
|
||||
import cn.hutool.core.img.ImgUtil;
|
||||
import cn.hutool.core.thread.ThreadUtil;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.awt.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
|
||||
class GrokServiceTest {
|
||||
|
||||
String key = "your key";
|
||||
GrokService grokService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.GROK.getValue()).setApiKey(key).build(), GrokService.class);
|
||||
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void chat(){
|
||||
final String chat = grokService.chat("写一个疯狂星期四广告词");
|
||||
assertNotNull(chat);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void chatStream() {
|
||||
String prompt = "写一个疯狂星期四广告词";
|
||||
// 使用AtomicBoolean作为结束标志
|
||||
AtomicBoolean isDone = new AtomicBoolean(false);
|
||||
|
||||
grokService.chat(prompt, data -> {
|
||||
assertNotNull(data);
|
||||
if (data.contains("[DONE]")) {
|
||||
// 设置结束标志
|
||||
isDone.set(true);
|
||||
} else if (data.contains("\"error\"")) {
|
||||
isDone.set(true);
|
||||
}
|
||||
|
||||
});
|
||||
// 轮询检查结束标志
|
||||
while (!isDone.get()) {
|
||||
ThreadUtil.sleep(100);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void testChat(){
|
||||
final List<Message> messages = new ArrayList<>();
|
||||
messages.add(new Message("system","你是个抽象大师,会说很抽象的话,最擅长说抽象的笑话"));
|
||||
messages.add(new Message("user","给我说一个笑话"));
|
||||
final String chat = grokService.chat(messages);
|
||||
assertNotNull(chat);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void message() {
|
||||
final String message = grokService.message("给我一个KFC的广告词", 4096);
|
||||
assertNotNull(message);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void messageStream() {
|
||||
String prompt = "给我一个KFC的广告词";
|
||||
|
||||
// 使用AtomicBoolean作为结束标志
|
||||
AtomicBoolean isDone = new AtomicBoolean(false);
|
||||
grokService.message(prompt, 4096, data -> {
|
||||
assertNotNull(data);
|
||||
if (data.contains("[DONE]")) {
|
||||
// 设置结束标志
|
||||
isDone.set(true);
|
||||
} else if (data.contains("\"error\"")) {
|
||||
isDone.set(true);
|
||||
}
|
||||
|
||||
});
|
||||
// 轮询检查结束标志
|
||||
while (!isDone.get()) {
|
||||
ThreadUtil.sleep(100);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void chatVision() {
|
||||
final GrokService grokService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.GROK.getValue()).setModel(Models.Grok.GROK_2_VISION_1212.getModel()).setApiKey(key).build(), GrokService.class);
|
||||
final String base64 = ImgUtil.toBase64DataUri(Toolkit.getDefaultToolkit().createImage("your imageUrl"), "png");
|
||||
final String chatVision = grokService.chatVision("图片上有些什么?", Arrays.asList(base64));
|
||||
assertNotNull(chatVision);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void testChatVisionStream() {
|
||||
final GrokService grokService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.GROK.getValue()).setModel(Models.Grok.GROK_2_VISION_1212.getModel()).setApiKey(key).build(), GrokService.class);
|
||||
String prompt = "图片上有些什么?";
|
||||
List<String> images = Arrays.asList("https://img2.baidu.com/it/u=862000265,4064861820&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=1544");
|
||||
|
||||
// 使用AtomicBoolean作为结束标志
|
||||
AtomicBoolean isDone = new AtomicBoolean(false);
|
||||
grokService.chatVision(prompt,images, data -> {
|
||||
assertNotNull(data);
|
||||
if (data.contains("[DONE]")) {
|
||||
// 设置结束标志
|
||||
isDone.set(true);
|
||||
} else if (data.contains("\"error\"")) {
|
||||
isDone.set(true);
|
||||
}
|
||||
|
||||
});
|
||||
// 轮询检查结束标志
|
||||
while (!isDone.get()) {
|
||||
ThreadUtil.sleep(100);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void testChatVision() {
|
||||
final GrokService grokService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.GROK.getValue()).setModel(Models.Grok.GROK_2_VISION_1212.getModel()).setApiKey(key).build(), GrokService.class);
|
||||
final String chatVision = grokService.chatVision("图片上有些什么?", Arrays.asList("https://img2.baidu.com/it/u=862000265,4064861820&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=1544"));
|
||||
assertNotNull(chatVision);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void models() {
|
||||
final String models = grokService.models();
|
||||
assertNotNull(models);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void getModel() {
|
||||
final String model = grokService.getModel("");
|
||||
assertNotNull(model);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void languageModels() {
|
||||
final String languageModels = grokService.languageModels();
|
||||
assertNotNull(languageModels);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void getLanguageModel() {
|
||||
final String language = grokService.getLanguageModel("");
|
||||
assertNotNull(language);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void tokenizeText() {
|
||||
final String tokenizeText = grokService.tokenizeText(key);
|
||||
assertNotNull(tokenizeText);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void deferredCompletion() {
|
||||
final String deferred = grokService.deferredCompletion(key);
|
||||
assertNotNull(deferred);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void imagesGenerations() {
|
||||
final GrokService grokService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.GROK.getValue())
|
||||
.setApiKey(key).setModel(Models.Grok.GROK_2_IMAGE.getModel()).build(), GrokService.class);
|
||||
final String imagesGenerations = grokService.imagesGenerations("一位年轻的宇航员站在未来感十足的太空站内,透过巨大的弧形落地窗凝望浩瀚宇宙。窗外,璀璨的星河与五彩斑斓的星云交织,远处隐约可见未知星球的轮廓,仿佛在召唤着探索的脚步。宇航服上的呼吸灯与透明显示屏上的星图交相辉映,象征着人类科技与宇宙奥秘的碰撞。画面深邃而神秘,充满对未知的渴望与无限可能的想象。");
|
||||
assertNotNull(imagesGenerations);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Hutool Team and hutool.cn
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package cn.hutool.ai.model.hutool;
|
||||
|
||||
import cn.hutool.ai.AIException;
|
||||
import cn.hutool.ai.AIServiceFactory;
|
||||
import cn.hutool.ai.ModelName;
|
||||
import cn.hutool.ai.core.AIConfigBuilder;
|
||||
import cn.hutool.ai.core.Message;
|
||||
import cn.hutool.core.img.ImgUtil;
|
||||
import cn.hutool.core.io.FileUtil;
|
||||
import cn.hutool.core.thread.ThreadUtil;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.awt.*;
|
||||
import java.io.File;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class HutoolServiceTest {
|
||||
|
||||
String key = "请前往Hutool-AI官网:https://ai.hutool.cn 获取";
|
||||
HutoolService hutoolService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.HUTOOL.getValue()).setApiKey(key).build(), HutoolService.class);
|
||||
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void chat(){
|
||||
final String chat = hutoolService.chat("写一个疯狂星期四广告词");
|
||||
assertNotNull(chat);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void chatStream() {
|
||||
String prompt = "写一个疯狂星期四广告词";
|
||||
// 使用AtomicBoolean作为结束标志
|
||||
AtomicBoolean isDone = new AtomicBoolean(false);
|
||||
|
||||
hutoolService.chat(prompt, data -> {
|
||||
assertNotNull(data);
|
||||
if (data.contains("[DONE]")) {
|
||||
// 设置结束标志
|
||||
isDone.set(true);
|
||||
} else if (data.contains("\"error\"")) {
|
||||
isDone.set(true);
|
||||
}
|
||||
|
||||
});
|
||||
// 轮询检查结束标志
|
||||
while (!isDone.get()) {
|
||||
ThreadUtil.sleep(100);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void testChat(){
|
||||
final List<Message> messages = new ArrayList<>();
|
||||
messages.add(new Message("system","你是个抽象大师,会说很抽象的话,最擅长说抽象的笑话"));
|
||||
messages.add(new Message("user","给我说一个笑话"));
|
||||
final String chat = hutoolService.chat(messages);
|
||||
assertNotNull(chat);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void chatVision() {
|
||||
final String base64 = ImgUtil.toBase64DataUri(Toolkit.getDefaultToolkit().createImage("your imageUrl"), "png");
|
||||
final String chatVision = hutoolService.chatVision("图片上有些什么?", Arrays.asList(base64));
|
||||
assertNotNull(chatVision);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void testChatVisionStream() {
|
||||
String prompt = "图片上有些什么?";
|
||||
List<String> images = Arrays.asList("https://img2.baidu.com/it/u=862000265,4064861820&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=1544");
|
||||
|
||||
// 使用AtomicBoolean作为结束标志
|
||||
AtomicBoolean isDone = new AtomicBoolean(false);
|
||||
hutoolService.chatVision(prompt,images, data -> {
|
||||
assertNotNull(data);
|
||||
if (data.contains("[DONE]")) {
|
||||
// 设置结束标志
|
||||
isDone.set(true);
|
||||
} else if (data.contains("\"error\"")) {
|
||||
isDone.set(true);
|
||||
}
|
||||
|
||||
});
|
||||
// 轮询检查结束标志
|
||||
while (!isDone.get()) {
|
||||
ThreadUtil.sleep(100);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void testChatVision() {
|
||||
final String chatVision = hutoolService.chatVision("图片上有些什么?", Arrays.asList("https://img2.baidu.com/it/u=862000265,4064861820&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=1544"));
|
||||
assertNotNull(chatVision);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void tokenizeText() {
|
||||
final String tokenizeText = hutoolService.tokenizeText(key);
|
||||
assertNotNull(tokenizeText);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void imagesGenerations() {
|
||||
final String imagesGenerations = hutoolService.imagesGenerations("一位年轻的宇航员站在未来感十足的太空站内,透过巨大的弧形落地窗凝望浩瀚宇宙。窗外,璀璨的星河与五彩斑斓的星云交织,远处隐约可见未知星球的轮廓,仿佛在召唤着探索的脚步。宇航服上的呼吸灯与透明显示屏上的星图交相辉映,象征着人类科技与宇宙奥秘的碰撞。画面深邃而神秘,充满对未知的渴望与无限可能的想象。");
|
||||
assertNotNull(imagesGenerations);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void embeddingVision() {
|
||||
final String embeddingVision = hutoolService.embeddingVision("天空好难", "https://img2.baidu.com/it/u=862000265,4064861820&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=1544");
|
||||
assertNotNull(embeddingVision);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void textToSpeech() {
|
||||
try {
|
||||
// 测试正常音频流返回
|
||||
final InputStream inputStream = hutoolService.tts("万里山河一夜白,\n" +
|
||||
"千峰尽染玉龙哀。\n" +
|
||||
"长风卷起琼花碎,\n" +
|
||||
"直上九霄揽月来。", HutoolCommon.HutoolSpeech.NOVA);
|
||||
assertNotNull(inputStream);
|
||||
|
||||
// 保存音频文件
|
||||
final String filePath = "your filePath";
|
||||
FileUtil.writeFromStream(inputStream, new File(filePath));
|
||||
|
||||
} catch (Exception e) {
|
||||
throw new AIException("TTS测试失败: " + e.getMessage());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void speechToText() {
|
||||
final File file = FileUtil.file("your filePath");
|
||||
final String speechToText = hutoolService.stt(file);
|
||||
assertNotNull(speechToText);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void videoTasks() {
|
||||
final String videoTasks = hutoolService.videoTasks("生成一段动画视频,主角是大耳朵图图,一个活泼可爱的小男孩。视频中图图在公园里玩耍," +
|
||||
"画面采用明亮温暖的卡通风格,色彩鲜艳,动作流畅。背景音乐轻快活泼,带有冒险感,音效包括鸟叫声、欢笑声和山洞回声。", "https://img2.baidu.com/it/u=862000265,4064861820&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=1544");
|
||||
assertNotNull(videoTasks);//cgt-20250529154621-d7dq9
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void getVideoTasksInfo() {
|
||||
final String videoTasksInfo = hutoolService.getVideoTasksInfo("cgt-20250529154621-d7dq9");
|
||||
assertNotNull(videoTasksInfo);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Hutool Team and hutool.cn
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package cn.hutool.ai.model.openai;
|
||||
|
||||
import cn.hutool.ai.AIServiceFactory;
|
||||
import cn.hutool.ai.ModelName;
|
||||
import cn.hutool.ai.Models;
|
||||
import cn.hutool.ai.core.AIConfigBuilder;
|
||||
import cn.hutool.ai.core.Message;
|
||||
import cn.hutool.core.io.FileUtil;
|
||||
import cn.hutool.core.thread.ThreadUtil;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
|
||||
class OpenaiServiceTest {
|
||||
|
||||
String key = "your key";
|
||||
OpenaiService openaiService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.OPENAI.getValue()).setApiKey(key).build(), OpenaiService.class);
|
||||
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void chat(){
|
||||
final String chat = openaiService.chat("写一个疯狂星期四广告词");
|
||||
assertNotNull(chat);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void chatStream() {
|
||||
String prompt = "写一个疯狂星期四广告词";
|
||||
// 使用AtomicBoolean作为结束标志
|
||||
AtomicBoolean isDone = new AtomicBoolean(false);
|
||||
|
||||
openaiService.chat(prompt, data -> {
|
||||
assertNotNull(data);
|
||||
if (data.contains("[DONE]")) {
|
||||
// 设置结束标志
|
||||
isDone.set(true);
|
||||
} else if (data.contains("\"error\"")) {
|
||||
isDone.set(true);
|
||||
}
|
||||
|
||||
});
|
||||
// 轮询检查结束标志
|
||||
while (!isDone.get()) {
|
||||
ThreadUtil.sleep(100);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void testChat(){
|
||||
final List<Message> messages = new ArrayList<>();
|
||||
messages.add(new Message("system","你是个抽象大师,会说很抽象的话,最擅长说抽象的笑话"));
|
||||
messages.add(new Message("user","给我说一个笑话"));
|
||||
final String chat = openaiService.chat(messages);
|
||||
assertNotNull(chat);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void chatVision() {
|
||||
final OpenaiService openaiService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.OPENAI.getValue())
|
||||
.setApiKey(key).setModel(Models.Openai.GPT_4O_MINI.getModel()).build(), OpenaiService.class);
|
||||
final String chatVision = openaiService.chatVision("图片上有些什么?", Arrays.asList("https://img2.baidu.com/it/u=862000265,4064861820&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=1544","https://img2.baidu.com/it/u=1682510685,1244554634&fm=253&fmt=auto&app=138&f=JPEG?w=803&h=800"));
|
||||
assertNotNull(chatVision);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void testChatVisionStream() {
|
||||
final OpenaiService openaiService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.OPENAI.getValue())
|
||||
.setApiKey(key).setModel(Models.Openai.GPT_4O_MINI.getModel()).build(), OpenaiService.class);
|
||||
String prompt = "图片上有些什么?";
|
||||
List<String> images = Arrays.asList("https://img2.baidu.com/it/u=862000265,4064861820&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=1544\",\"https://img2.baidu.com/it/u=1682510685,1244554634&fm=253&fmt=auto&app=138&f=JPEG?w=803&h=800");
|
||||
|
||||
// 使用AtomicBoolean作为结束标志
|
||||
AtomicBoolean isDone = new AtomicBoolean(false);
|
||||
openaiService.chatVision(prompt,images, data -> {
|
||||
assertNotNull(data);
|
||||
if (data.contains("[DONE]")) {
|
||||
// 设置结束标志
|
||||
isDone.set(true);
|
||||
} else if (data.contains("\"error\"")) {
|
||||
isDone.set(true);
|
||||
}
|
||||
|
||||
});
|
||||
// 轮询检查结束标志
|
||||
while (!isDone.get()) {
|
||||
ThreadUtil.sleep(100);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void imagesGenerations() {
|
||||
final OpenaiService openaiService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.OPENAI.getValue())
|
||||
.setApiKey(key).setModel(Models.Openai.DALL_E_3.getModel()).build(), OpenaiService.class);
|
||||
final String imagesGenerations = openaiService.imagesGenerations("一位年轻的宇航员站在未来感十足的太空站内,透过巨大的弧形落地窗凝望浩瀚宇宙。窗外,璀璨的星河与五彩斑斓的星云交织,远处隐约可见未知星球的轮廓,仿佛在召唤着探索的脚步。宇航服上的呼吸灯与透明显示屏上的星图交相辉映,象征着人类科技与宇宙奥秘的碰撞。画面深邃而神秘,充满对未知的渴望与无限可能的想象。");
|
||||
assertNotNull(imagesGenerations);
|
||||
//https://oaidalleapiprodscus.blob.core.windows.net/private/org-l99H6T0zCZejctB2TqdYrXFB/user-LilDVU1V8cUxJYwVAGRkUwYd/img-yA9kNatHnBiUHU5lZGim1hP2.png?st=2025-03-07T01%3A04%3A18Z&se=2025-03-07T03%3A04%3A18Z&sp=r&sv=2024-08-04&sr=b&rscd=inline&rsct=image/png&skoid=d505667d-d6c1-4a0a-bac7-5c84a87759f8&sktid=a48cca56-e6da-484e-a814-9c849652bcb3&skt=2025-03-06T15%3A04%3A42Z&ske=2025-03-07T15%3A04%3A42Z&sks=b&skv=2024-08-04&sig=rjcRzC5U7Y3pEDZ4ME0CiviAPdIpoGO2rRTXw3m8rHw%3D
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void imagesEdits() {
|
||||
final OpenaiService openaiService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.OPENAI.getValue())
|
||||
.setApiKey(key).setModel(Models.Openai.DALL_E_2.getModel()).build(), OpenaiService.class);
|
||||
final File file = FileUtil.file("your imgUrl");
|
||||
final String imagesEdits = openaiService.imagesEdits("茂密的森林中,有一只九色鹿若隐若现",file);
|
||||
assertNotNull(imagesEdits);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void imagesVariations() {
|
||||
final OpenaiService openaiService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.OPENAI.getValue())
|
||||
.setApiKey(key).setModel(Models.Openai.DALL_E_2.getModel()).build(), OpenaiService.class);
|
||||
final File file = FileUtil.file("your imgUrl");
|
||||
final String imagesVariations = openaiService.imagesVariations(file);
|
||||
assertNotNull(imagesVariations);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void textToSpeech() {
|
||||
final OpenaiService openaiService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.OPENAI.getValue())
|
||||
.setApiKey(key).setModel(Models.Openai.TTS_1_HD.getModel()).build(), OpenaiService.class);
|
||||
final InputStream inputStream = openaiService.textToSpeech("万里山河一夜白,\n" +
|
||||
"千峰尽染玉龙哀。\n" +
|
||||
"长风卷起琼花碎,\n" +
|
||||
"直上九霄揽月来。", OpenaiCommon.OpenaiSpeech.NOVA);
|
||||
|
||||
final String filePath = "your filePath";
|
||||
final Path path = Paths.get(filePath);
|
||||
try (final FileOutputStream outputStream = new FileOutputStream(filePath)) {
|
||||
Files.createDirectories(path.getParent());
|
||||
final byte[] buffer = new byte[1024];
|
||||
int bytesRead;
|
||||
while ((bytesRead = inputStream.read(buffer)) != -1) {
|
||||
outputStream.write(buffer, 0, bytesRead);
|
||||
}
|
||||
} catch (final IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void speechToText() {
|
||||
final OpenaiService openaiService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.OPENAI.getValue())
|
||||
.setApiKey(key).setModel(Models.Openai.WHISPER_1.getModel()).build(), OpenaiService.class);
|
||||
final File file = FileUtil.file("your filePath");
|
||||
final String speechToText = openaiService.speechToText(file);
|
||||
assertNotNull(speechToText);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void embeddingText() {
|
||||
final OpenaiService openaiService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.OPENAI.getValue())
|
||||
.setApiKey(key).setModel(Models.Openai.TEXT_EMBEDDING_3_SMALL.getModel()).build(), OpenaiService.class);
|
||||
final String embeddingText = openaiService.embeddingText("萬里山河一夜白,千峰盡染玉龍哀,長風捲起瓊花碎,直上九霄闌月來");
|
||||
assertNotNull(embeddingText);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void moderations() {
|
||||
final OpenaiService openaiService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.OPENAI.getValue())
|
||||
.setApiKey(key).setModel(Models.Openai.OMNI_MODERATION_LATEST.getModel()).build(), OpenaiService.class);
|
||||
final String moderations = openaiService.moderations("你要杀人", "https://img2.baidu.com/it/u=862000265,4064861820&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=1544");
|
||||
assertNotNull(moderations);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void chatReasoning() {
|
||||
final OpenaiService openaiService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.OPENAI.getValue())
|
||||
.setApiKey(key).setModel(Models.Openai.O3_MINI.getModel()).build(), OpenaiService.class);
|
||||
final List<Message> messages = new ArrayList<>();
|
||||
messages.add(new Message("system","你是现代抽象家"));
|
||||
messages.add(new Message("user","给我一个KFC疯狂星期四的文案"));
|
||||
final String chatReasoning = openaiService.chatReasoning(messages, OpenaiCommon.OpenaiReasoning.HIGH.getEffort());
|
||||
assertNotNull(chatReasoning);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void chatReasoningStream() {
|
||||
final OpenaiService openaiService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.OPENAI.getValue())
|
||||
.setApiKey(key).setModel(Models.Openai.O3_MINI.getModel()).build(), OpenaiService.class);
|
||||
final List<Message> messages = new ArrayList<>();
|
||||
messages.add(new Message("system","你是现代抽象家"));
|
||||
messages.add(new Message("user","给我一个KFC疯狂星期四的文案"));
|
||||
|
||||
// 使用AtomicBoolean作为结束标志
|
||||
AtomicBoolean isDone = new AtomicBoolean(false);
|
||||
openaiService.chatReasoning(messages,OpenaiCommon.OpenaiReasoning.HIGH.getEffort(), data -> {
|
||||
assertNotNull(data);
|
||||
if (data.contains("[DONE]")) {
|
||||
// 设置结束标志
|
||||
isDone.set(true);
|
||||
} else if (data.contains("\"error\"")) {
|
||||
isDone.set(true);
|
||||
}
|
||||
|
||||
});
|
||||
// 轮询检查结束标志
|
||||
while (!isDone.get()) {
|
||||
ThreadUtil.sleep(100);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@
|
||||
<parent>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-parent</artifactId>
|
||||
<version>5.8.38-SNAPSHOT</version>
|
||||
<version>5.8.39</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>hutool-all</artifactId>
|
||||
@@ -113,6 +113,11 @@
|
||||
<artifactId>hutool-jwt</artifactId>
|
||||
<version>${project.parent.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-ai</artifactId>
|
||||
<version>${project.parent.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<parent>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-parent</artifactId>
|
||||
<version>5.8.38-SNAPSHOT</version>
|
||||
<version>5.8.39</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>hutool-aop</artifactId>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<parent>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-parent</artifactId>
|
||||
<version>5.8.38-SNAPSHOT</version>
|
||||
<version>5.8.39</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>hutool-bloomFilter</artifactId>
|
||||
|
||||
@@ -29,7 +29,7 @@ public class BitSetBloomFilter implements BloomFilter {
|
||||
*
|
||||
* @param c 当前过滤器预先开辟的最大包含记录,通常要比预计存入的记录多一倍.
|
||||
* @param n 当前过滤器预计所要包含的记录.
|
||||
* @param k 哈希函数的个数,等同每条记录要占用的bit数.
|
||||
* @param k 哈希函数的个数,等同每条记录要占用的bit数,此处值取值为1~8
|
||||
*/
|
||||
public BitSetBloomFilter(int c, int n, int k) {
|
||||
this.hashFunctionNumber = k;
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<parent>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-parent</artifactId>
|
||||
<version>5.8.38-SNAPSHOT</version>
|
||||
<version>5.8.39</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>hutool-bom</artifactId>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<parent>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-parent</artifactId>
|
||||
<version>5.8.38-SNAPSHOT</version>
|
||||
<version>5.8.39</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>hutool-cache</artifactId>
|
||||
|
||||
@@ -88,7 +88,9 @@ public abstract class AbstractCache<K, V> implements Cache<K, V> {
|
||||
final MutableObj<K> mKey = MutableObj.of(key);
|
||||
|
||||
// issue#3618 对于替换的键值对,不做满队列检查和清除
|
||||
if (cacheMap.containsKey(mKey)) {
|
||||
final CacheObj<K, V> oldObj = cacheMap.get(mKey);
|
||||
if (null != oldObj) {
|
||||
onRemove(oldObj.key, oldObj.obj);
|
||||
// 存在相同key,覆盖之
|
||||
cacheMap.put(mKey, co);
|
||||
} else {
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
package cn.hutool.cache.impl;
|
||||
|
||||
import cn.hutool.core.collection.CopiedIter;
|
||||
import cn.hutool.core.lang.mutable.Mutable;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
/**
|
||||
@@ -81,7 +84,14 @@ public abstract class ReentrantCache<K, V> extends AbstractCache<K, V> {
|
||||
public void clear() {
|
||||
lock.lock();
|
||||
try {
|
||||
cacheMap.clear();
|
||||
// 获取所有键的副本
|
||||
Set<Mutable<K>> keys = new HashSet<>(cacheMap.keySet());
|
||||
for (Mutable<K> key : keys) {
|
||||
CacheObj<K, V> co = removeWithoutLock(key.get());
|
||||
if (co != null) {
|
||||
onRemove(co.key, co.obj); // 触发资源释放
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
|
||||
@@ -4,13 +4,13 @@ import cn.hutool.cache.impl.TimedCache;
|
||||
import cn.hutool.core.date.DateUnit;
|
||||
import cn.hutool.core.thread.ThreadUtil;
|
||||
import cn.hutool.core.util.RandomUtil;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* 缓存测试用例
|
||||
*
|
||||
@@ -148,4 +148,22 @@ public class CacheTest {
|
||||
assertFalse(ALARM_CACHE.containsKey(1));
|
||||
assertEquals(1, counter.get());
|
||||
}
|
||||
|
||||
/**
|
||||
* ReentrantCache类clear()方法、AbstractCache.putWithoutLock方法可能导致资源泄露
|
||||
* https://github.com/chinabugotech/hutool/issues/3957
|
||||
*/
|
||||
@Test
|
||||
public void reentrantCache_clear_Method_Test() {
|
||||
final AtomicInteger removeCount = new AtomicInteger();
|
||||
final Cache<String, String> lruCache = CacheUtil.newLRUCache(4);
|
||||
lruCache.setListener((key, cachedObject) -> removeCount.getAndIncrement());
|
||||
lruCache.put("key1","String1");
|
||||
lruCache.put("key2","String2");
|
||||
lruCache.put("key3","String3");
|
||||
lruCache.put("key1","String4");//key已经存在,原始putWithoutLock方法存在资源泄露
|
||||
lruCache.put("key4","String5");
|
||||
lruCache.clear();//ReentrantCache类clear()方法存在资源泄露
|
||||
Assertions.assertEquals(5, removeCount.get());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<parent>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-parent</artifactId>
|
||||
<version>5.8.38-SNAPSHOT</version>
|
||||
<version>5.8.39</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>hutool-captcha</artifactId>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<parent>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-parent</artifactId>
|
||||
<version>5.8.38-SNAPSHOT</version>
|
||||
<version>5.8.39</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>hutool-core</artifactId>
|
||||
|
||||
@@ -12,6 +12,7 @@ import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
@@ -48,7 +49,11 @@ public class BeanDesc implements Serializable {
|
||||
public BeanDesc(Class<?> beanClass) {
|
||||
Assert.notNull(beanClass);
|
||||
this.beanClass = beanClass;
|
||||
init();
|
||||
if(RecordUtil.isRecord(beanClass)){
|
||||
initForRecord();
|
||||
}else{
|
||||
init();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -153,6 +158,27 @@ public class BeanDesc implements Serializable {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 针对Record类的反射初始化
|
||||
*/
|
||||
private void initForRecord() {
|
||||
final Class<?> beanClass = this.beanClass;
|
||||
final Map<String, PropDesc> propMap = this.propMap;
|
||||
|
||||
final List<Method> getters = ReflectUtil.getPublicMethods(beanClass, method -> 0 == method.getParameterCount());
|
||||
// 排除静态属性和对象子类
|
||||
final Field[] fields = ReflectUtil.getFields(beanClass, field -> !ModifierUtil.isStatic(field) && !ReflectUtil.isOuterClassField(field));
|
||||
for (final Field field : fields) {
|
||||
for (final Method getter : getters) {
|
||||
if (field.getName().equals(getter.getName())) {
|
||||
//record对象,getter方法与字段同名
|
||||
final PropDesc prop = new PropDesc(field, getter, null);
|
||||
propMap.putIfAbsent(prop.getFieldName(), prop);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据字段创建属性描述<br>
|
||||
* 查找Getter和Setter方法时会:
|
||||
|
||||
110
hutool-core/src/main/java/cn/hutool/core/bean/RecordUtil.java
Normal file
110
hutool-core/src/main/java/cn/hutool/core/bean/RecordUtil.java
Normal file
@@ -0,0 +1,110 @@
|
||||
package cn.hutool.core.bean;
|
||||
|
||||
import cn.hutool.core.bean.copier.ValueProvider;
|
||||
import cn.hutool.core.util.ClassUtil;
|
||||
import cn.hutool.core.util.JdkUtil;
|
||||
import cn.hutool.core.util.ReflectUtil;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.AbstractMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* java.lang.Record 相关工具类封装<br>
|
||||
* 来自于FastJSON2的BeanUtils
|
||||
*
|
||||
* @author fastjson2, Looly
|
||||
* @since 5.8.38
|
||||
*/
|
||||
public class RecordUtil {
|
||||
|
||||
private static volatile Class<?> RECORD_CLASS;
|
||||
|
||||
private static volatile Method METHOD_GET_RECORD_COMPONENTS;
|
||||
private static volatile Method METHOD_COMPONENT_GET_NAME;
|
||||
private static volatile Method METHOD_COMPONENT_GET_GENERIC_TYPE;
|
||||
|
||||
/**
|
||||
* 判断给定类是否为Record类
|
||||
*
|
||||
* @param clazz 类
|
||||
* @return 是否为Record类
|
||||
*/
|
||||
public static boolean isRecord(final Class<?> clazz) {
|
||||
if (JdkUtil.JVM_VERSION < 14) {
|
||||
// JDK14+支持Record类
|
||||
return false;
|
||||
}
|
||||
final Class<?> superClass = clazz.getSuperclass();
|
||||
if (superClass == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (RECORD_CLASS == null) {
|
||||
// 此处不使用同步代码,重复赋值并不影响判断
|
||||
final String superclassName = superClass.getName();
|
||||
if ("java.lang.Record".equals(superclassName)) {
|
||||
RECORD_CLASS = superClass;
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return superClass == RECORD_CLASS;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取Record类中所有字段名称,getter方法名与字段同名
|
||||
*
|
||||
* @param recordClass Record类
|
||||
* @return 字段数组
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public static Map.Entry<String, Type>[] getRecordComponents(final Class<?> recordClass) {
|
||||
if (JdkUtil.JVM_VERSION < 14) {
|
||||
// JDK14+支持Record类
|
||||
return new Map.Entry[0];
|
||||
}
|
||||
if (null == METHOD_GET_RECORD_COMPONENTS) {
|
||||
METHOD_GET_RECORD_COMPONENTS = ReflectUtil.getMethod(Class.class, "getRecordComponents");
|
||||
}
|
||||
|
||||
final Class<Object> recordComponentClass = ClassUtil.loadClass("java.lang.reflect.RecordComponent");
|
||||
if (METHOD_COMPONENT_GET_NAME == null) {
|
||||
METHOD_COMPONENT_GET_NAME = ReflectUtil.getMethod(recordComponentClass, "getName");
|
||||
}
|
||||
if (METHOD_COMPONENT_GET_GENERIC_TYPE == null) {
|
||||
METHOD_COMPONENT_GET_GENERIC_TYPE = ReflectUtil.getMethod(recordComponentClass, "getGenericType");
|
||||
}
|
||||
|
||||
final Object[] components = ReflectUtil.invoke(recordClass, METHOD_GET_RECORD_COMPONENTS);
|
||||
final Map.Entry<String, Type>[] entries = new Map.Entry[components.length];
|
||||
for (int i = 0; i < components.length; i++) {
|
||||
entries[i] = new AbstractMap.SimpleEntry<>(
|
||||
ReflectUtil.invoke(components[i], METHOD_COMPONENT_GET_NAME),
|
||||
ReflectUtil.invoke(components[i], METHOD_COMPONENT_GET_GENERIC_TYPE)
|
||||
);
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
/**
|
||||
* 实例化Record类
|
||||
*
|
||||
* @param recordClass 类
|
||||
* @param valueProvider 参数值提供器
|
||||
* @return Record类
|
||||
*/
|
||||
public static Object newInstance(final Class<?> recordClass, final ValueProvider<String> valueProvider) {
|
||||
final Map.Entry<String, Type>[] recordComponents = getRecordComponents(recordClass);
|
||||
final Object[] args = new Object[recordComponents.length];
|
||||
for (int i = 0; i < args.length; i++) {
|
||||
args[i] = valueProvider.value(recordComponents[i].getKey(), recordComponents[i].getValue());
|
||||
}
|
||||
|
||||
return ReflectUtil.newInstance(recordClass, args);
|
||||
}
|
||||
}
|
||||
@@ -16,10 +16,8 @@ import java.util.Map;
|
||||
@SuppressWarnings("rawtypes")
|
||||
public class BeanToMapCopier extends AbsCopier<Object, Map> {
|
||||
|
||||
/**
|
||||
* 目标的Map类型(用于泛型类注入)
|
||||
*/
|
||||
private final Type targetType;
|
||||
// 提前获取目标值真实类型
|
||||
private final Type[] targetTypeArguments;
|
||||
|
||||
/**
|
||||
* 构造
|
||||
@@ -31,7 +29,7 @@ public class BeanToMapCopier extends AbsCopier<Object, Map> {
|
||||
*/
|
||||
public BeanToMapCopier(Object source, Map target, Type targetType, CopyOptions copyOptions) {
|
||||
super(source, target, copyOptions);
|
||||
this.targetType = targetType;
|
||||
this.targetTypeArguments = TypeUtil.getTypeArguments(targetType);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -68,11 +66,10 @@ public class BeanToMapCopier extends AbsCopier<Object, Map> {
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取目标值真实类型并转换源值
|
||||
final Type[] typeArguments = TypeUtil.getTypeArguments(this.targetType);
|
||||
if(null != typeArguments && typeArguments.length > 1){
|
||||
// 尝试转换源值
|
||||
if(null != targetTypeArguments && targetTypeArguments.length > 1){
|
||||
//sValue = Convert.convertWithCheck(typeArguments[1], sValue, null, this.copyOptions.ignoreError);
|
||||
sValue = this.copyOptions.convertField(typeArguments[1], sValue);
|
||||
sValue = this.copyOptions.convertField(targetTypeArguments[1], sValue);
|
||||
}
|
||||
|
||||
// 自定义值
|
||||
|
||||
@@ -13,10 +13,8 @@ import java.util.Map;
|
||||
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||
public class MapToMapCopier extends AbsCopier<Map, Map> {
|
||||
|
||||
/**
|
||||
* 目标的类型(用于泛型类注入)
|
||||
*/
|
||||
private final Type targetType;
|
||||
// 提前获取目标值真实类型
|
||||
private final Type[] targetTypeArguments;
|
||||
|
||||
/**
|
||||
* 构造
|
||||
@@ -28,7 +26,7 @@ public class MapToMapCopier extends AbsCopier<Map, Map> {
|
||||
*/
|
||||
public MapToMapCopier(Map source, Map target, Type targetType, CopyOptions copyOptions) {
|
||||
super(source, target, copyOptions);
|
||||
this.targetType = targetType;
|
||||
targetTypeArguments = TypeUtil.getTypeArguments(targetType);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -57,11 +55,10 @@ public class MapToMapCopier extends AbsCopier<Map, Map> {
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取目标值真实类型并转换源值
|
||||
final Type[] typeArguments = TypeUtil.getTypeArguments(this.targetType);
|
||||
if (null != typeArguments) {
|
||||
// 尝试转换源值
|
||||
if (null != targetTypeArguments) {
|
||||
//sValue = Convert.convertWithCheck(typeArguments[1], sValue, null, this.copyOptions.ignoreError);
|
||||
sValue = this.copyOptions.convertField(typeArguments[1], sValue);
|
||||
sValue = this.copyOptions.convertField(targetTypeArguments[1], sValue);
|
||||
}
|
||||
|
||||
// 自定义值
|
||||
|
||||
@@ -31,7 +31,7 @@ import java.util.stream.Collectors;
|
||||
* 日期时间工具类
|
||||
*
|
||||
* @author xiaoleilu
|
||||
* @see LocalDateTimeUtil java8日志工具类
|
||||
* @see LocalDateTimeUtil java8日期工具类
|
||||
* @see DatePattern 日期常用格式工具类
|
||||
*/
|
||||
public class DateUtil extends CalendarUtil {
|
||||
|
||||
@@ -14,7 +14,7 @@ import java.util.Date;
|
||||
import java.util.TimeZone;
|
||||
|
||||
/**
|
||||
* JDK8+中的{@link LocalDateTime} 工具类封装
|
||||
* JDK8+中的{@link LocalDateTime}工具类封装
|
||||
*
|
||||
* @author looly
|
||||
* @see DatePattern 常用格式工具类
|
||||
|
||||
@@ -59,7 +59,6 @@ public class LunarFestival {
|
||||
// 七月
|
||||
L_FTV.put(new Pair<>(7, 7), "七夕");
|
||||
L_FTV.put(new Pair<>(7, 14), "鬼节(南方)");
|
||||
L_FTV.put(new Pair<>(7, 15), "中元节");
|
||||
L_FTV.put(new Pair<>(7, 15), "盂兰盆节 中元节");
|
||||
L_FTV.put(new Pair<>(7, 30), "地藏节");
|
||||
|
||||
|
||||
@@ -539,6 +539,68 @@ public class Assert {
|
||||
return noNullElements(array, "[Assertion failed] - this array must not contain any null elements");
|
||||
}
|
||||
|
||||
/**
|
||||
* 断言给定集合为空
|
||||
* 并使用指定的函数获取错误信息返回
|
||||
* <pre class="code">
|
||||
* Assert.empty(collection, ()->{
|
||||
* // to query relation message
|
||||
* return new IllegalArgumentException("relation message to return");
|
||||
* });
|
||||
* </pre>
|
||||
*
|
||||
* @param <E> 集合元素类型
|
||||
* @param <T> 集合类型
|
||||
* @param <X> 异常类型
|
||||
* @param collection 被检查的集合
|
||||
* @param errorSupplier 错误抛出异常附带的消息生产接口
|
||||
* @throws X if the collection is not {@code null} or has elements
|
||||
* @see CollUtil#isEmpty(Iterable)
|
||||
* @since 5.8.39
|
||||
*/
|
||||
public static <E, T extends Iterable<E>, X extends Throwable> void empty(T collection, Supplier<X> errorSupplier) throws X {
|
||||
if (CollUtil.isNotEmpty(collection)) {
|
||||
throw errorSupplier.get();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 断言给定集合为空
|
||||
*
|
||||
* <pre class="code">
|
||||
* Assert.empty(collection, "Collection must have no elements");
|
||||
* </pre>
|
||||
*
|
||||
* @param <E> 集合元素类型
|
||||
* @param <T> 集合类型
|
||||
* @param collection 被检查的集合
|
||||
* @param errorMsgTemplate 异常时的消息模板
|
||||
* @param params 参数列表
|
||||
* @throws IllegalArgumentException if the collection is not {@code null} or has elements
|
||||
* @since 5.8.39
|
||||
*/
|
||||
public static <E, T extends Iterable<E>> void empty(T collection, String errorMsgTemplate, Object... params) throws IllegalArgumentException {
|
||||
empty(collection, () -> new IllegalArgumentException(StrUtil.format(errorMsgTemplate, params)));
|
||||
}
|
||||
|
||||
/**
|
||||
* 断言给定集合为空
|
||||
*
|
||||
* <pre class="code">
|
||||
* Assert.empty(collection);
|
||||
* </pre>
|
||||
*
|
||||
* @param <E> 集合元素类型
|
||||
* @param <T> 集合类型
|
||||
* @param collection 被检查的集合
|
||||
* @throws IllegalArgumentException if the collection is not {@code null} or has elements
|
||||
* @since 5.8.39
|
||||
*/
|
||||
public static <E, T extends Iterable<E>> void empty(T collection) throws IllegalArgumentException {
|
||||
empty(collection, "[Assertion failed] - this collection must be empty");
|
||||
}
|
||||
|
||||
/**
|
||||
* 断言给定集合非空
|
||||
* 并使用指定的函数获取错误信息返回
|
||||
|
||||
@@ -653,7 +653,7 @@ public class Dict extends LinkedHashMap<String, Object> implements BasicTypeGett
|
||||
* @param key KEY
|
||||
* @return 小写KEY
|
||||
*/
|
||||
private String customKey(String key) {
|
||||
protected String customKey(String key) {
|
||||
if (this.caseInsensitive && null != key) {
|
||||
key = key.toLowerCase();
|
||||
}
|
||||
|
||||
@@ -91,8 +91,6 @@ public class PatternPool {
|
||||
public final static Pattern TEL = Pattern.compile(RegexPool.TEL);
|
||||
/**
|
||||
* 座机号码+400+800电话
|
||||
*
|
||||
* @see <a href="https://baike.baidu.com/item/800">800</a>
|
||||
*/
|
||||
public final static Pattern TEL_400_800 = Pattern.compile(RegexPool.TEL_400_800);
|
||||
/**
|
||||
|
||||
@@ -87,10 +87,8 @@ public interface RegexPool {
|
||||
String TEL = "(010|02\\d|0[3-9]\\d{2})-?(\\d{6,8})";
|
||||
/**
|
||||
* 座机号码+400+800电话
|
||||
*
|
||||
* @see <a href="https://baike.baidu.com/item/800">800</a>
|
||||
*/
|
||||
String TEL_400_800 = "0\\d{2,3}[\\- ]?[1-9]\\d{6,7}|[48]00[\\- ]?[1-9]\\d{2}[\\- ]?\\d{4}";
|
||||
String TEL_400_800 = "0\\d{2,3}[\\- ]?[0-9]\\d{6,7}|[48]00[\\- ]?[0-9]\\d{2}[\\- ]?\\d{4}";
|
||||
/**
|
||||
* 18位身份证号码
|
||||
*/
|
||||
|
||||
@@ -40,7 +40,7 @@ import java.util.Random;
|
||||
*
|
||||
* @since 4.1.11
|
||||
*/
|
||||
public class UUID implements java.io.Serializable, Comparable<UUID> {
|
||||
public final class UUID implements java.io.Serializable, Comparable<UUID> {
|
||||
private static final long serialVersionUID = -1185015143654744140L;
|
||||
|
||||
/**
|
||||
|
||||
@@ -294,7 +294,7 @@ public class Money implements Serializable, Comparable<Money> {
|
||||
*/
|
||||
public void setAmount(BigDecimal amount) {
|
||||
if (amount != null) {
|
||||
cent = rounding(amount.movePointRight(2), DEFAULT_ROUNDING_MODE);
|
||||
cent = rounding(amount.movePointRight(currency.getDefaultFractionDigits()), DEFAULT_ROUNDING_MODE);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -726,7 +726,7 @@ public class Money implements Serializable, Comparable<Money> {
|
||||
Money lowResult = newMoneyWithSameCurrency(cent / targets);
|
||||
Money highResult = newMoneyWithSameCurrency(lowResult.cent + 1);
|
||||
|
||||
int remainder = (int) cent % targets;
|
||||
int remainder = (int) (cent % targets);
|
||||
|
||||
for (int i = 0; i < remainder; i++) {
|
||||
results[i] = highResult;
|
||||
|
||||
@@ -4244,8 +4244,22 @@ public class CharSequenceUtil {
|
||||
* @return 转换后的字符串
|
||||
* @see String#toLowerCase()
|
||||
* @since 5.8.38
|
||||
* @deprecated 拼写错误,请使用 {@link #toLowerCase(CharSequence)}
|
||||
*/
|
||||
@Deprecated
|
||||
public static String toLoweCase(final CharSequence str) {
|
||||
return toLowerCase(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将字符串转为小写
|
||||
*
|
||||
* @param str 被转的字符串
|
||||
* @return 转换后的字符串
|
||||
* @see String#toLowerCase()
|
||||
* @since 5.8.39
|
||||
*/
|
||||
public static String toLowerCase(final CharSequence str) {
|
||||
if (null == str) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,308 @@
|
||||
package cn.hutool.core.thread;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* 可召回批处理线程池执行器
|
||||
* <pre>
|
||||
* 1.数据分批并行处理
|
||||
* 2.主线程、线程池混合执行批处理任务,主线程空闲时会尝试召回线程池队列中的任务执行
|
||||
* 3.线程安全,可用同时执行多个任务,线程池满载时,效率与单线程模式相当,无阻塞风险,无脑提交任务即可
|
||||
* </pre>
|
||||
*
|
||||
* 适用场景:
|
||||
* <pre>
|
||||
* 1.批量处理数据且需要同步结束的场景,能一定程度上提高吞吐量、防止任务堆积 {@link #process(List, int, Function)}
|
||||
* 2.普通查询接口加速 {@link #processByWarp(Warp[])}
|
||||
* </pre>
|
||||
*
|
||||
* @author likuan
|
||||
*/
|
||||
public class RecyclableBatchThreadPoolExecutor {
|
||||
|
||||
private final ExecutorService executor;
|
||||
|
||||
/**
|
||||
* 构造
|
||||
*
|
||||
* @param poolSize 线程池大小
|
||||
*/
|
||||
public RecyclableBatchThreadPoolExecutor(int poolSize){
|
||||
this(poolSize,"recyclable-batch-pool-");
|
||||
}
|
||||
|
||||
/**
|
||||
* 建议的构造方法
|
||||
* <pre>
|
||||
* 1.使用无界队列,主线程会召回队列中的任务执行,不会有任务堆积,无需考虑拒绝策略
|
||||
* 2.假如在web场景中请求量过大导致oom,不使用此工具也会有同样的结果,甚至更严重,应该对请求做限制或做其他优化
|
||||
* </pre>
|
||||
*
|
||||
* @param poolSize 线程池大小
|
||||
* @param threadPoolPrefix 线程名前缀
|
||||
*/
|
||||
public RecyclableBatchThreadPoolExecutor(int poolSize, String threadPoolPrefix){
|
||||
AtomicInteger threadNumber = new AtomicInteger(1);
|
||||
ThreadFactory threadFactory = r -> {
|
||||
Thread t = new Thread(r, threadPoolPrefix + threadNumber.getAndIncrement());
|
||||
if (t.isDaemon()) {
|
||||
t.setDaemon(false);
|
||||
}
|
||||
if (t.getPriority() != Thread.NORM_PRIORITY) {
|
||||
t.setPriority(Thread.NORM_PRIORITY);
|
||||
}
|
||||
return t;
|
||||
};
|
||||
this.executor = new ThreadPoolExecutor(poolSize, poolSize, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(),threadFactory);
|
||||
}
|
||||
|
||||
/**
|
||||
* 自定义线程池,一般不需要使用
|
||||
* @param executor 线程池
|
||||
*/
|
||||
public RecyclableBatchThreadPoolExecutor(ExecutorService executor){
|
||||
this.executor = executor;
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭线程池
|
||||
*/
|
||||
public void shutdown(){
|
||||
executor.shutdown();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取线程池
|
||||
* @return ExecutorService
|
||||
*/
|
||||
public ExecutorService getExecutor(){
|
||||
return executor;
|
||||
}
|
||||
|
||||
/**
|
||||
* 分批次处理数据
|
||||
* <pre>
|
||||
* 1.所有批次执行完成后会过滤null并返回合并结果,保持输入数据顺序,不需要结果{@link Function}返回null即可
|
||||
* 2.{@link Function}需自行处理异常、保证线程安全
|
||||
* 3.原始数据在分片后可能被外部修改,导致批次数据不一致,如有必要,传参之前进行数据拷贝
|
||||
* 4.主线程会参与处理批次数据,如果要异步执行任务请使用普通线程池
|
||||
* </pre>
|
||||
*
|
||||
* @param <T> 输入数据类型
|
||||
* @param <R> 输出数据类型
|
||||
* @param data 待处理数据集合
|
||||
* @param batchSize 每批次数据量
|
||||
* @param processor 单条数据处理函数
|
||||
* @return 处理结果集合
|
||||
*/
|
||||
public <T,R> List<R> process(List<T> data, int batchSize, Function<T,R> processor) {
|
||||
if (batchSize < 1) {
|
||||
throw new IllegalArgumentException("batchSize >= 1");
|
||||
}
|
||||
List<List<T>> batches = splitData(data, batchSize);
|
||||
int batchCount = batches.size();
|
||||
int minusOne = batchCount - 1;
|
||||
ArrayDeque<IdempotentTask<R>> taskQueue = new ArrayDeque<>(minusOne);
|
||||
Map<Integer,Future<TaskResult<R>>> futuresMap = new HashMap<>();
|
||||
// 提交前 batchCount-1 批任务
|
||||
for (int i = 0 ; i < minusOne ; i++) {
|
||||
final int index = i;
|
||||
IdempotentTask<R> task = new IdempotentTask<>(i,() -> processBatch(batches.get(index), processor));
|
||||
taskQueue.add(task);
|
||||
futuresMap.put(i,executor.submit(task));
|
||||
}
|
||||
@SuppressWarnings("unchecked")
|
||||
List<R>[] resultArr = new ArrayList[batchCount];
|
||||
// 处理最后一批
|
||||
resultArr[minusOne] = processBatch(batches.get(minusOne), processor);
|
||||
// 处理剩余任务
|
||||
processRemainingTasks(taskQueue, futuresMap,resultArr);
|
||||
//排序、过滤null
|
||||
return Stream.of(resultArr)
|
||||
.filter(Objects::nonNull)
|
||||
.flatMap(List::stream)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理剩余任务并收集结果
|
||||
* @param taskQueue 任务队列
|
||||
* @param futuresMap 异步任务映射
|
||||
* @param resultArr 结果存储数组
|
||||
*/
|
||||
private <R> void processRemainingTasks(Queue<IdempotentTask<R>> taskQueue, Map<Integer,Future<TaskResult<R>>> futuresMap, List<R>[] resultArr) {
|
||||
// 主消费未执行任务
|
||||
IdempotentTask<R> task;
|
||||
while ((task = taskQueue.poll()) != null) {
|
||||
try {
|
||||
TaskResult<R> call = task.call();
|
||||
if (call.effective) {
|
||||
// 取消被主线程执行任务
|
||||
Future<TaskResult<R>> future = futuresMap.remove(task.index);
|
||||
future.cancel(false);
|
||||
//加入结果集
|
||||
resultArr[task.index] = call.result;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// 不处理异常
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
futuresMap.forEach((index,future)->{
|
||||
try {
|
||||
TaskResult<R> taskResult = future.get();
|
||||
if(taskResult.effective){
|
||||
resultArr[index] = taskResult.result;
|
||||
}
|
||||
} catch (InterruptedException | ExecutionException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 幂等任务包装类,确保任务只执行一次
|
||||
*/
|
||||
private static class IdempotentTask<R> implements Callable<TaskResult<R>> {
|
||||
|
||||
private final int index;
|
||||
private final Callable<List<R>> delegate;
|
||||
private final AtomicBoolean executed = new AtomicBoolean(false);
|
||||
|
||||
IdempotentTask(int index,Callable<List<R>> delegate) {
|
||||
this.index = index;
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TaskResult<R> call() throws Exception {
|
||||
if (executed.compareAndSet(false, true)) {
|
||||
return new TaskResult<>(delegate.call(), true);
|
||||
}
|
||||
return new TaskResult<>(null, false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 结果包装类,标记结果有效性
|
||||
*/
|
||||
private static class TaskResult<R>{
|
||||
private final List<R> result;
|
||||
private final boolean effective;
|
||||
TaskResult(List<R> result, boolean effective){
|
||||
this.result = result;
|
||||
this.effective = effective;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据分片方法
|
||||
* @param data 原始数据
|
||||
* @param batchSize 每批次数据量
|
||||
* @return 分片后的二维集合
|
||||
*/
|
||||
private static <T> List<List<T>> splitData(List<T> data, int batchSize) {
|
||||
int batchCount = (data.size() + batchSize - 1) / batchSize;
|
||||
return new AbstractList<List<T>>() {
|
||||
@Override
|
||||
public List<T> get(int index) {
|
||||
int from = index * batchSize;
|
||||
int to = Math.min((index + 1) * batchSize, data.size());
|
||||
return data.subList(from, to);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
return batchCount;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 单批次数据处理
|
||||
* @param batch 单批次数据
|
||||
* @param processor 处理函数
|
||||
* @return 处理结果
|
||||
*/
|
||||
private static <T,R> List<R> processBatch(List<T> batch, Function<T,R> processor) {
|
||||
return batch.stream().map(processor).filter(Objects::nonNull).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理Warp数组
|
||||
*
|
||||
* <pre>{@code
|
||||
* Warp<String> warp1 = Warp.of(this::select1);
|
||||
* Warp<List<String>> warp2 = Warp.of(this::select2);
|
||||
* executor.processByWarp(warp1, warp2);
|
||||
* String r1 = warp1.get();
|
||||
* List<String> r2 = warp2.get();
|
||||
* }</pre>
|
||||
*
|
||||
* @param warps Warp数组
|
||||
* @return Warp集合,此方法返回结果为空的不会被过滤
|
||||
*/
|
||||
public List<Warp<?>> processByWarp(Warp<?>... warps) {
|
||||
return processByWarp(Arrays.asList(warps));
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理Warp集合
|
||||
* @param warps Warp集合
|
||||
* @return Warp集合,此方法返回结果为空的不会被过滤
|
||||
*/
|
||||
public List<Warp<?>> processByWarp(List<Warp<?>> warps) {
|
||||
return process(warps, 1, Warp::execute);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理逻辑包装类
|
||||
* @param <R> 结果类型
|
||||
*/
|
||||
public static class Warp<R>{
|
||||
|
||||
private Warp(Supplier<R> supplier){
|
||||
Objects.requireNonNull(supplier);
|
||||
this.supplier = supplier;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建Warp
|
||||
* @param supplier 执行逻辑
|
||||
* @return Warp
|
||||
* @param <R> 结果类型
|
||||
*/
|
||||
public static <R> Warp<R> of(Supplier<R> supplier){
|
||||
return new Warp<>(supplier);
|
||||
}
|
||||
|
||||
private final Supplier<R> supplier;
|
||||
private R result;
|
||||
|
||||
/**
|
||||
* 获取结果
|
||||
* @return 结果
|
||||
*/
|
||||
public R get() {
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行
|
||||
* @return this
|
||||
*/
|
||||
public Warp<R> execute() {
|
||||
result = supplier.get();
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
package cn.hutool.core.thread;
|
||||
|
||||
import cn.hutool.core.io.IORuntimeException;
|
||||
import cn.hutool.core.util.RuntimeUtil;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.Thread.UncaughtExceptionHandler;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.CompletionService;
|
||||
@@ -322,7 +324,9 @@ public class ThreadUtil {
|
||||
try {
|
||||
timeUnit.sleep(timeout.longValue());
|
||||
} catch (InterruptedException e) {
|
||||
return false;
|
||||
// 重新标记线程为中断状态(恢复中断信息),让后续代码能感知到“线程曾被中断过”
|
||||
Thread.currentThread().interrupt();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -352,6 +356,8 @@ public class ThreadUtil {
|
||||
try {
|
||||
Thread.sleep(millis);
|
||||
} catch (InterruptedException e) {
|
||||
// 重新标记线程为中断状态(恢复中断信息),让后续代码能感知到“线程曾被中断过”
|
||||
Thread.currentThread().interrupt();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -506,7 +512,8 @@ public class ThreadUtil {
|
||||
thread.join();
|
||||
dead = true;
|
||||
} catch (InterruptedException e) {
|
||||
// ignore
|
||||
// 重新标记线程为中断状态(恢复中断信息),让后续代码能感知到“线程曾被中断过”
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
} while (false == dead);
|
||||
}
|
||||
@@ -613,7 +620,8 @@ public class ThreadUtil {
|
||||
try {
|
||||
obj.wait();
|
||||
} catch (InterruptedException e) {
|
||||
// ignore
|
||||
// 重新标记线程为中断状态(恢复中断信息),让后续代码能感知到“线程曾被中断过”
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -629,10 +637,13 @@ public class ThreadUtil {
|
||||
* @return {@link ConcurrencyTester}
|
||||
* @since 4.5.8
|
||||
*/
|
||||
@SuppressWarnings("resource")
|
||||
public static ConcurrencyTester concurrencyTest(int threadSize, Runnable runnable) {
|
||||
return (new ConcurrencyTester(threadSize)).test(runnable);
|
||||
}
|
||||
try (ConcurrencyTester tester = new ConcurrencyTester(threadSize)) {
|
||||
return tester.test(runnable);
|
||||
} catch (IOException e) {
|
||||
throw new IORuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建{@link ScheduledThreadPoolExecutor}
|
||||
|
||||
@@ -917,7 +917,9 @@ public class ClassUtil {
|
||||
&& false == clazz.isArray() //
|
||||
&& false == clazz.isAnnotation() //
|
||||
&& false == clazz.isSynthetic() //
|
||||
&& false == clazz.isPrimitive();//
|
||||
&& false == clazz.isPrimitive()//
|
||||
// issue#3965 String有isEmpty方法,但是不能被当作bean
|
||||
&& clazz != String.class;//
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -75,6 +75,14 @@ public class DesensitizedUtil {
|
||||
* IPv6地址
|
||||
*/
|
||||
IPV6,
|
||||
/**
|
||||
* 护照号
|
||||
*/
|
||||
PASSPORT,
|
||||
/**
|
||||
* 统一社会信用代码
|
||||
*/
|
||||
CREDIT_CODE,
|
||||
/**
|
||||
* 定义了一个first_mask的规则,只显示第一个字符。
|
||||
*/
|
||||
@@ -153,6 +161,12 @@ public class DesensitizedUtil {
|
||||
case IPV6:
|
||||
newStr = ipv6(String.valueOf(str));
|
||||
break;
|
||||
case PASSPORT:
|
||||
newStr = passport(String.valueOf(str));
|
||||
break;
|
||||
case CREDIT_CODE:
|
||||
newStr = creditCode(String.valueOf(str));
|
||||
break;
|
||||
case FIRST_MASK:
|
||||
newStr = firstMask(String.valueOf(str));
|
||||
break;
|
||||
@@ -397,4 +411,30 @@ public class DesensitizedUtil {
|
||||
public static String ipv6(String ipv6) {
|
||||
return StrUtil.subBefore(ipv6, ':', false) + ":*:*:*:*:*:*:*";
|
||||
}
|
||||
|
||||
/**
|
||||
* 护照号脱敏
|
||||
* 规则:前2后2,长度不足时保留最小有效信息
|
||||
* 示例:PJ1234567 → PJ*****67
|
||||
*/
|
||||
public static String passport(String passport) {
|
||||
if (StrUtil.isBlank(passport)) return passport;
|
||||
final int length = passport.length();
|
||||
if (length <= 2) return StrUtil.hide(passport, 0, length);
|
||||
return StrUtil.hide(passport, 2, length - 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一社会信用代码脱敏
|
||||
* 规则:前4后4,长度不足时保留最小有效信息
|
||||
* 统一社会信用代码由18位数字或大写英文字母组成
|
||||
* 示例:91110108MA01ABCDE7 → 9111**********CDE7
|
||||
*
|
||||
*/
|
||||
public static String creditCode(String code) {
|
||||
if (StrUtil.isBlank(code)) return code;
|
||||
final int length = code.length();
|
||||
if (length <= 4) return StrUtil.hide(code, 0, length);
|
||||
return StrUtil.hide(code, 4, length - 4);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1266,7 +1266,7 @@ public class NumberUtil {
|
||||
}
|
||||
if (chars[i] == 'l' || chars[i] == 'L') {
|
||||
// not allowing L with an exponent
|
||||
return foundDigit && !hasExp;
|
||||
return foundDigit && !hasExp && !hasDecPoint;
|
||||
}
|
||||
// last character is illegal
|
||||
return false;
|
||||
|
||||
@@ -153,7 +153,7 @@ public class ReflectUtil {
|
||||
|
||||
/**
|
||||
* 获取指定类中字段名和字段对应的有序Map,包括其父类中的字段<br>
|
||||
* 如果子类与父类中存在同名字段,则这两个字段同时存在,子类字段在前,父类字段在后。
|
||||
* 如果子类与父类中存在同名字段,则后者覆盖前者。
|
||||
*
|
||||
* @param beanClass 类
|
||||
* @return 字段名和字段对应的Map,有序
|
||||
|
||||
@@ -926,7 +926,7 @@ public class ZipUtil {
|
||||
*/
|
||||
public static byte[] zlib(InputStream in, int level, int length) {
|
||||
final ByteArrayOutputStream out = new ByteArrayOutputStream(length);
|
||||
Deflate.of(in, out, false).deflater(level);
|
||||
Deflate.of(in, out, false).deflater(level).close();
|
||||
return out.toByteArray();
|
||||
}
|
||||
|
||||
@@ -974,7 +974,7 @@ public class ZipUtil {
|
||||
*/
|
||||
public static byte[] unZlib(InputStream in, int length) {
|
||||
final ByteArrayOutputStream out = new ByteArrayOutputStream(length);
|
||||
Deflate.of(in, out, false).inflater();
|
||||
Deflate.of(in, out, false).inflater().close();
|
||||
return out.toByteArray();
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,9 @@ import cn.hutool.core.util.StrUtil;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class AssertTest {
|
||||
|
||||
@Test
|
||||
@@ -68,4 +71,13 @@ public class AssertTest {
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void emptyCollectionTest() {
|
||||
List<Object> testList = new ArrayList<>();
|
||||
Assertions.assertDoesNotThrow(() -> Assert.empty(null));
|
||||
Assertions.assertDoesNotThrow(() -> Assert.empty(testList));
|
||||
testList.add(new Object());
|
||||
Assertions.assertThrows(IllegalArgumentException.class, () -> Assert.empty(testList));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -165,6 +165,8 @@ public class ValidatorTest {
|
||||
public void isPlateNumberTest() {
|
||||
assertTrue(Validator.isPlateNumber("粤BA03205"));
|
||||
assertTrue(Validator.isPlateNumber("闽20401领"));
|
||||
//issue#3979
|
||||
assertTrue(Validator.isPlateNumber("沪AE22075"));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -2,6 +2,8 @@ package cn.hutool.core.math;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Currency;
|
||||
|
||||
public class MoneyTest {
|
||||
|
||||
@@ -20,4 +22,12 @@ public class MoneyTest {
|
||||
|
||||
assertEquals(1234.56D, MathUtil.centToYuan(123456), 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void currencyScalingTest() {
|
||||
Money jpyMoney = new Money(0, Currency.getInstance("JPY"));
|
||||
jpyMoney.setAmount(BigDecimal.ONE);
|
||||
assertEquals(1, jpyMoney.getCent());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
package cn.hutool.core.thread;
|
||||
|
||||
import cn.hutool.core.thread.RecyclableBatchThreadPoolExecutor.Warp;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* {@link RecyclableBatchThreadPoolExecutor} 测试类
|
||||
*/
|
||||
public class RecyclableBatchThreadPoolExecutorTest {
|
||||
|
||||
|
||||
/**
|
||||
* 批量处理数据
|
||||
*/
|
||||
@Test
|
||||
@Disabled
|
||||
public void test() throws InterruptedException {
|
||||
int corePoolSize = 10;// 线程池大小
|
||||
int batchSize = 100;// 每批次数据量
|
||||
int clientCount = 30;// 调用者数量
|
||||
test(corePoolSize,batchSize,clientCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* 普通查询接口加速
|
||||
*/
|
||||
@Test
|
||||
@Disabled
|
||||
public void test2() {
|
||||
RecyclableBatchThreadPoolExecutor executor = new RecyclableBatchThreadPoolExecutor(10);
|
||||
long s = System.nanoTime();
|
||||
Warp<String> warp1 = Warp.of(this::select1);
|
||||
Warp<List<String>> warp2 = Warp.of(this::select2);
|
||||
executor.processByWarp(warp1, warp2);
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
map.put("key1",warp1.get());
|
||||
map.put("key2",warp2.get());
|
||||
long d = System.nanoTime() - s;
|
||||
System.out.printf("总耗时:%.2f秒%n",d/1e9);
|
||||
System.out.println(map);
|
||||
}
|
||||
|
||||
public void test(int corePoolSize,int batchSize,int clientCount ) throws InterruptedException{
|
||||
RecyclableBatchThreadPoolExecutor processor = new RecyclableBatchThreadPoolExecutor(corePoolSize);
|
||||
// 模拟多个调用者线程提交任务
|
||||
ExecutorService testExecutor = Executors.newFixedThreadPool(clientCount);
|
||||
Map<Integer, List<Integer>> map = new HashMap<>();
|
||||
for(int i = 0; i < clientCount; i++){
|
||||
map.put(i,testDate(1000));
|
||||
}
|
||||
long s = System.nanoTime();
|
||||
List<Future<?>> futures = new ArrayList<>();
|
||||
for (int j = 0; j < clientCount; j++) {
|
||||
final int clientId = j;
|
||||
Future<?> submit = testExecutor.submit(() -> {
|
||||
Function<Integer, String> function = p -> {
|
||||
try {
|
||||
Thread.sleep(10);
|
||||
} catch (InterruptedException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
return Thread.currentThread().getName() + "#" + p;
|
||||
};
|
||||
long start = System.nanoTime();
|
||||
List<String> process = processor.process(map.get(clientId), batchSize, function);
|
||||
long duration = System.nanoTime() - start;
|
||||
System.out.printf("【clientId:%s】处理结果:%s\n处理耗时:%.2f秒%n", clientId, process, duration / 1e9);
|
||||
});
|
||||
futures.add(submit);
|
||||
}
|
||||
futures.forEach(p-> {
|
||||
try {
|
||||
p.get();
|
||||
} catch (InterruptedException | ExecutionException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
});
|
||||
long d = System.nanoTime() - s;
|
||||
System.out.printf("总耗时:%.2f秒%n",d/1e9);
|
||||
testExecutor.shutdown();
|
||||
processor.shutdown();
|
||||
}
|
||||
public static List<Integer> testDate(int count){
|
||||
List<Integer> list = new ArrayList<>();
|
||||
for(int i = 1;i<=count;i++){
|
||||
list.add(i);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
private String select1() {
|
||||
try {
|
||||
Thread.sleep(3000);
|
||||
} catch (InterruptedException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
return "1";
|
||||
}
|
||||
|
||||
private List<String> select2() {
|
||||
try {
|
||||
Thread.sleep(5000);
|
||||
} catch (InterruptedException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
return Arrays.asList("1","2","3");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
package cn.hutool.core.util;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
|
||||
/**
|
||||
* 脱敏工具类 DesensitizedUtils 安全测试
|
||||
*
|
||||
@@ -102,4 +104,19 @@ public class DesensitizedUtilTest {
|
||||
assertEquals("1234 **** **** **** 678", DesensitizedUtil.bankCard("1234 2222 3333 4444 678"));
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void passportTest(){
|
||||
assertEquals(null, DesensitizedUtil.passport(null));
|
||||
assertEquals("", DesensitizedUtil.passport(""));
|
||||
assertEquals("EM*****67", DesensitizedUtil.passport("EM1234567"));
|
||||
assertEquals("*", DesensitizedUtil.passport("3"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void creditCodeTest(){
|
||||
assertEquals(null, DesensitizedUtil.creditCode(null));
|
||||
assertEquals("", DesensitizedUtil.creditCode(""));
|
||||
assertEquals("9111**********CDE7", DesensitizedUtil.creditCode("91110108MA01ABCDE7"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package cn.hutool.core.util;
|
||||
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class IssueICA9S5Test {
|
||||
@Test
|
||||
public void test() {
|
||||
String a = "ENUM{\\ndisable ~ 0\\nenable ~ 1\\n}";
|
||||
final List<String> split = StrUtil.split(a, "\\n");
|
||||
Assertions.assertEquals(4, split.size());
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user