91 Commits

Author SHA1 Message Date
bugo
f6ba182f52 🚀release5.8.39 2025-06-23 10:47:39 +08:00
Looly
cd40a65195 update central-publishing-maven-plugin 2025-06-20 18:40:13 +08:00
Looly
c738c2b42b 增加可召回批处理线程池执行器RecyclableBatchThreadPoolExecutor(pr#1343@Gitee) 2025-06-20 17:54:40 +08:00
Looly
0c19f0b9a4 !1343 可召回批处理线程池执行器,主线程、线程池混合执行批处理任务,主线程空闲时会尝试召回线程池队列中的任务执行
Merge pull request !1343 from lk/v5-dev
2025-06-20 09:50:11 +00:00
Looly
a504fa860c 修复AbstractCacheputWithoutLock方法可能导致的外部资源泄露问题(pr#3958@Github) 2025-06-20 17:34:24 +08:00
Golden Looly
a60c70ca86 Merge pull request #3958 from IcoreE/yanzhongxin-v5-dev
AbstractCache.putWithoutLock方法可能导致的外部资源泄露问题
2025-06-20 17:28:14 +08:00
Looly
2493b8da8b Db添加FetchSize的全局设置(pr#3978@Github) 2025-06-20 11:43:19 +08:00
Golden Looly
a10181dab2 Merge pull request #3978 from yry0304/v5-dev
Db添加FetchSize的全局设置,用户可以根据内存性能自主调节JDBC每次结果集获取的记录数量,海量数据加载时可提高性能
2025-06-20 11:41:22 +08:00
Golden Looly
28b21d7617 Merge pull request #3970 from asukavuuyn/v5-dev
fix(Money): currency scaling bug(币种小数位硬编码)
2025-06-20 11:06:37 +08:00
Looly
8deed41367 修复Money类的setAmount方法没有获取当前币种的小数位数而是使用的默认小数位和在遇到非2小数位的币种(如日元使用 0 位)会导致金额设置错误问题(pr#3970@Github) 2025-06-20 11:05:41 +08:00
Looly
3b759bfae1 !1362 test(ReflectUtilTest): ReflectUtil#getFieldMap 如果子类与父类中存在同名字段,则后者覆盖前者。
Merge pull request !1362 from tanpenggood/v5-master
2025-06-20 03:01:21 +00:00
Looly
0c9c6ce655 add comment 2025-06-20 10:46:57 +08:00
Looly
5919837386 修复ZipUtil中zlib和unZlib调用后资源未释放问题(issue#3976@Github) 2025-06-20 10:43:09 +08:00
Looly
46249c257f add test 2025-06-20 10:33:52 +08:00
杨若瑜
a368ab544d Db添加FetchSize的全局设置,用户可以根据内存性能自主调节JDBC每次结果集获取的记录数量,海量数据加载时可提高性能 2025-06-20 00:47:21 +08:00
asukavuuyn
6a58bfe9b2 fix(Money): currency scaling bug 2025-06-16 23:42:17 +08:00
Looly
41141cd824 gts 2025-06-14 19:02:25 +08:00
Looly
2a602d0bd4 修复TEL_400_800正则规则太窄问题(issue#3967@Github) 2025-06-14 18:40:25 +08:00
tanpenggood
6c8fc623f0 test(ReflectUtilTest): ReflectUtil#getFieldMap 如果子类与父类中存在同名字段,则后者覆盖前者。 2025-06-12 01:50:00 +08:00
Looly
484cdb9e4f 修复ThreadUtil中中断异常处理丢失中断信息的问题,解决ConcurrencyTester资源未释放的问题(pr#1358@Gitee) 2025-06-09 16:57:05 +08:00
Looly
62be1b0fe0 !1358 fix:解决threadutil中中断异常处理丢失中断信息的问题,解决ConcurrencyTester资源未释放的问题
Merge pull request !1358 from konggang/fix-threadutil
2025-06-09 08:56:06 +00:00
Looly
d015404248 Assert新增断言给定集合为空的方法以及单元测试用例(pr#3952@Github) 2025-06-09 12:22:21 +08:00
Golden Looly
d786c8e62d Merge pull request #3952 from baofeidyz/v5-dev
Assert新增断言给定集合为空的方法以及单元测试用例
2025-06-09 12:20:53 +08:00
Looly
e1d68e61df Merge branch 'v5-dev' of gitee.com:dromara/hutool into v5-dev 2025-06-09 12:10:38 +08:00
Looly
2c8070f324 HttpConfig增加参数setIgnoreContentLength可选忽略读取响应contentLength头(issue#ICB1B8@Gitee) 2025-06-09 12:01:29 +08:00
eli_chow
abae998de5 !1360 同步CHANGELOG信息
Merge pull request !1360 from eli_chow/v5-dev
2025-06-09 03:44:28 +00:00
choweli
86e5f19197 同步CHANGELOG信息 2025-06-09 11:42:58 +08:00
Looly
fe597605cf 修复LunarFestival中重复节日问题(issue#ICC8X3@Gitee) 2025-06-09 11:32:19 +08:00
yanzhongxin
5931264706 修复ReentrantCache的clear存在的外部资源泄露问题 2025-06-06 15:05:55 +08:00
chinabugotech
37fb3afce7 更新百度统计代码
Signed-off-by: chinabugotech <bugo@bugotech.cn>
2025-06-05 06:56:47 +00:00
孔纲
4cf41c444f fix:解决threadutil中中断异常处理丢失中断信息的问题,解决ConcurrencyTester资源未释放的问题 2025-06-05 10:20:18 +08:00
yanzhongxin
7739f8b015 修复putWithoutLock存在的外部资源泄露问题 2025-06-04 15:03:11 +08:00
eli_chow
6d851488de !1355 增加超时时间和读取超时时间设置
Merge pull request !1355 from eli_chow/v5-dev
2025-06-04 02:59:17 +00:00
choweli
82399a185e 增加超时时间和读取超时时间设置 2025-06-04 10:58:13 +08:00
eli_chow
3be0111884 !1354 Hutool-AI接口地址修改
Merge pull request !1354 from eli_chow/v5-dev
2025-06-03 02:33:28 +00:00
choweli
017a7b5c98 Hutool-AI接口地址修改 2025-06-03 10:31:16 +08:00
eli_chow
04f9784d08 !1352 新增Hutool-AI服务
Merge pull request !1352 from eli_chow/v5-dev
2025-05-30 06:50:37 +00:00
choweli
008b9fd662 新增Hutool-AI服务 2025-05-30 14:42:24 +08:00
choweli
322c079abf 新增Hutool-AI服务 2025-05-30 14:36:48 +08:00
baofeidyz
5b10fedfef Assert新增断言给定集合为空的方法以及单元测试用例 2025-05-29 11:19:01 +08:00
Looly
420e54a37d 修复ExcelPicUtil中可能的空指针异常 2025-05-27 12:20:15 +08:00
Looly
7f3be3038a add test 2025-05-26 08:37:49 +08:00
Looly
a34637f071 优化XXXToMapCopier的部分性能(pr#1345@Gitee) 2025-05-23 21:54:05 +08:00
Looly
dc54b89b87 优化XXXToMapCopier的部分性能(pr#1345@Gitee) 2025-05-23 21:47:23 +08:00
Looly
4e1b468096 !1345 优化XXXToMapCopier的部分性能
Merge pull request !1345 from IzayoiYurin/v5-dev
2025-05-23 13:46:36 +00:00
Looly
5f5ead1129 修复Money中金额分配的问题bug(issue#IC9Y35@Gitee) 2025-05-23 21:41:18 +08:00
Looly
95ee9a0cb5 DesensitizedUtil新增护照号码脱敏功能(pr#1347@Gitee) 2025-05-23 21:37:38 +08:00
Looly
8f0777eadd !1347 feat: DesensitizedUtil新增护照号码脱敏功能
Merge pull request !1347 from konggang/v5-dev
2025-05-23 13:36:41 +00:00
Looly
cb0645dff9 Merge branch 'v5-dev' of github.com:dromara/hutool into v5-dev 2025-05-23 16:24:18 +08:00
Golden Looly
c99616fa56 Merge pull request #3949 from bling-yshs/v5-dev
docs: 修复错字,删除意外的空格
2025-05-23 16:24:13 +08:00
Looly
0426686dcb Merge branch 'v5-dev' of gitee.com:dromara/hutool into v5-dev 2025-05-23 16:24:11 +08:00
孔纲
d8c9f1a06f feat: DesensitizedUtil新增护照号码脱敏功能
feat: DesensitizedUtil新增社会信用代码脱敏功能
2025-05-23 16:19:59 +08:00
bling-yshs
2f3be3ebdb docs: 修复错字,删除意外的空格 2025-05-23 16:07:30 +08:00
eli_chow
ce232cdd4f !1346 排除多余依赖,代码优化
Merge pull request !1346 from eli_chow/v5-dev
2025-05-23 08:03:47 +00:00
choweli
2b35ae8d09 排除多余依赖,代码优化 2025-05-23 16:01:37 +08:00
Golden Looly
f498459961 Merge pull request #3945 from TouyamaRie/patch-1
错误标点修改
2025-05-23 15:50:05 +08:00
Looly
cc543bcc5d 修复UUIDequals的问题,改为final类 2025-05-23 15:46:30 +08:00
Looly
6cd936078b remove invalid dependency 2025-05-23 15:44:07 +08:00
Yurin
e65e006b24 优化XXXToMapCopier的部分性能 2025-05-22 17:17:38 +08:00
likuan
1f2dc4fd3a 可召回批处理线程池执行器,增加包装类处理方法 2025-05-22 15:04:55 +08:00
likuan
7a2ef283ff 可召回批处理线程池执行器,增加处理逻辑包装类,增加包装类处理方法 2025-05-22 14:44:46 +08:00
likuan
d1988d4db9 可召回批处理线程池执行器,更换任务队列,完善接口文档 2025-05-21 13:46:51 +08:00
likuan
2d7a64f660 可召回批处理线程池执行器测试类 2025-05-20 16:29:38 +08:00
likuan
8580250ce8 可召回批处理线程池执行器 2025-05-20 15:50:40 +08:00
No one
5ee00acaad 错误标点修改
修改JWT.java中的错误标点,将[;]修改为[:]
2025-05-19 17:07:52 +08:00
eli_chow
e7d5a3b00f !1341 增加SSE流式返回函数参数callback,豆包、grok新增文生图接口,豆包生成视频支持使用model
Merge pull request !1341 from eli_chow/v5-dev
2025-05-19 05:06:16 +00:00
choweli
d1314d5905 增加SSE流式返回函数参数callback,豆包、grok新增文生图接口,豆包生成视频支持使用model 2025-05-19 12:00:33 +08:00
Looly
a49523ffab 修复CharsequenceUtiltoLowerCase方法拼写错误(issue#3941@Github) 2025-05-15 16:45:23 +08:00
Looly
f0c7b4d8f0 修复NumberUtilisNumber方法以L结尾没有小数点判断问题(issue#3938@Github) 2025-05-13 15:02:32 +08:00
Looly
0ceed43e2e Merge branch 'v5-dev' of gitee.com:dromara/hutool into v5-dev 2025-05-13 14:47:59 +08:00
Looly
00cd1ac1cd 🚀release5.8.38 2025-05-13 14:47:28 +08:00
bugo
0ed9c08507 🐢prepare 2025-05-13 14:46:13 +08:00
bugo
abbe514479 🚀release5.8.38 2025-05-13 13:35:15 +08:00
choweli
87b69cf076 新增hutool-ai模块 2025-05-13 11:06:40 +08:00
choweli
13d0edc957 增加hutool-ai模块,对AI大模型的封装实现(pr#3937@Github) 2025-05-13 10:38:13 +08:00
elichow
12e29629cd Merge pull request #3937 from elichow/v5-dev
新特性hutool-ai模块
2025-05-13 10:22:41 +08:00
choweli
45226d78ad 新增hutool-ai模块 2025-05-13 10:14:36 +08:00
Looly
6f346fc9a1 Dict的customKey方法访问权限修改为protected(pr#1340@Gitee) 2025-05-12 13:15:23 +08:00
Looly
de5e7cc35d !1340 refactor(Dict): 将 customKey 方法的访问权限修改为 protected
Merge pull request !1340 from 蒋小小/v5-dev
2025-05-12 05:12:54 +00:00
Looly
fc4a71200a 修正SshjSftp在SftpSubsystem服务时报错问题(pr#1338@Gitee) 2025-05-12 12:15:27 +08:00
Looly
25327585b1 !1338 解决pwd、cd调用command导致仅SftpSubsystem服务时无法正常使用的问题
Merge pull request !1338 from 厉军/v5-dev
2025-05-12 04:08:42 +00:00
bwcx_jzy
f19f2f39e3 refactor(Dict): 将 customKey 方法的访问权限修改为 protected
- 将 customKey 方法的访问权限从 private 修改为 protected
- 此修改可能旨在允许子类访问和重写该方法,增加代码的灵活性和可扩展性
2025-05-12 10:37:49 +08:00
Looly
a76bfc8338 修复某些数据库的getParameterMetaData会返回NULL,导致空指针的问题。(pr#3936@Github) 2025-05-12 09:17:11 +08:00
Golden Looly
ed180873a5 Merge pull request #3936 from yisiliang/v5-master
某些数据库的getParameterMetaData 会返回 NULL,然后导致报错,也需要规避这种情况
2025-05-12 09:15:22 +08:00
yisiliang
3c49d9524a 某些数据库的getParameterMetaData 会返回 NULL,然后导致报错,也需要规避这种情况 2025-05-09 18:59:31 +08:00
Looly
b8e6b1ecc0 添加RecordUtil支持record类(issue#3931@Github) 2025-05-07 10:23:16 +08:00
厉军
2b777c3841 避免重复rename函数,增加新单元测试SshjSftpTest,SftpTest用于测试Sftp类 2025-04-30 16:22:14 +08:00
厉军
e458bca2a0 Merge remote-tracking branch 'origin/feature/ftp-rename' into v5-dev
# Conflicts:
#	hutool-extra/src/main/java/cn/hutool/extra/ftp/AbstractFtp.java
#	hutool-extra/src/test/java/cn/hutool/extra/ssh/SftpTest.java
2025-04-30 15:29:45 +08:00
厉军
6e2e137b91 测试代码调整 2025-04-30 15:28:18 +08:00
厉军
81159f75bb 解决pwd、cd调用command导致仅SftpSubsystem服务时无法正常使用的问题 2025-04-30 15:20:52 +08:00
厉军
1e9c92c015 FTP接口增加rename方法,改文件/目录名 2025-04-24 16:43:39 +08:00
129 changed files with 7434 additions and 120 deletions

View File

@@ -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`中金额分配的问题bugissue#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方法访问权限修改为protectedpr#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)

View File

@@ -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 encapsulationcomposing 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.

View File

@@ -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平台没有测试不能保证所有工具类或工具方法可用。

View File

@@ -1 +1 @@
5.8.38
5.8.39

View File

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

View File

@@ -1 +1 @@
var version = '5.8.38'
var version = '5.8.39'

45
hutool-ai/pom.xml Normal file
View 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>

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

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

View 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 由目前为止的对话组成的消息列表可以设置rolecontent。详细参考官方文档
* @return AI模型返回的Response响应字符串
* @since 5.8.38
*/
public static String chat(final AIConfig config, final List<Message> messages) {
return getAIService(config).chat(messages);
}
}

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

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

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

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

View File

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

View 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 由目前为止的对话组成的消息列表可以设置rolecontent。详细参考官方文档
* @return AI回答
* @since 5.8.38
*/
String chat(final List<Message> messages);
/**
* 对话-SSE流式输出
* @param messages 由目前为止的对话组成的消息列表可以设置rolecontent。详细参考官方文档
* @param callback 流式数据回调函数
* @since 5.8.39
*/
void chat(final List<Message> messages, final Consumer<String> callback);
}

View File

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

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

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

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

View 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;

View File

@@ -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 {
}

View File

@@ -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";
}
}

View File

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

View 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.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();
}

View File

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

View 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.
*/
/**
* 对deepSeek的封装实现
*
* @author elichow
* @since 5.8.38
*/
package cn.hutool.ai.model.deepseek;

View File

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

View File

@@ -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";
}
}

View File

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

View File

@@ -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为您创建的推理接入点EndpointID。详细参考官方文档
*
* @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为生成视频的模型或者您创建的推理接入点EndpointID。详细参考官方文档
*
* @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为您创建的批量推理接入点EndpointID。详细参考官方文档
* 该方法不支持流式
*
* @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为您创建的批量推理接入点EndpointID。详细参考官方文档
* 该方法不支持流式
*
* @param messages 由对话组成的消息列表。如系统人设,背景信息等,用户自定义的信息
* @return AI回答
* @since 5.8.38
*/
String batchChat(final List<Message> messages);
/**
* 创建上下文缓存: 创建上下文缓存,获得缓存 id字段后在上下文缓存对话 API中使用。
* 注意调用该方法时配置config中的model为您创建的推理接入点EndpointID,
* 推理接入点中使用的模型需要在模型管理中开启缓存功能。详细参考官方文档
*
* @param messages 由对话组成的消息列表。如系统人设,背景信息等,用户自定义的信息
* @param mode 上下文缓存的类型,详细参考官方文档 默认为session
* @return 返回的缓存id
* @since 5.8.38
*/
String createContext(final List<Message> messages, String mode);
/**
* 创建上下文缓存: 创建上下文缓存,获得缓存 id字段后在上下文缓存对话 API中使用。
* 注意调用该方法时配置config中的model为您创建的推理接入点EndpointID,
* 推理接入点中使用的模型需要在模型管理中开启缓存功能。详细参考官方文档
*
* @param messages 由对话组成的消息列表。如系统人设,背景信息等,用户自定义的信息
* @return 返回的缓存id
* @since 5.8.38
*/
default String createContext(final List<Message> messages) {
return createContext(messages, DoubaoCommon.DoubaoContext.SESSION.getMode());
}
/**
* 上下文缓存对话: 向大模型发起带上下文缓存的请求
* 注意配置config中的model可以为您创建的推理接入点EndpointID也可以是支持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可以为您创建的推理接入点EndpointID也可以是支持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可以为您创建的推理接入点EndpointID也可以是支持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可以为您创建的推理接入点EndpointID也可以是支持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);
}

View File

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

View 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.
*/
/**
* 对doubao的封装实现
*
* @author elichow
* @since 5.8.38
*/
package cn.hutool.ai.model.doubao;

View File

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

View File

@@ -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";
}
}

View File

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

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

View File

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

View 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.
*/
/**
* 对grok的封装实现
*
* @author elichow
* @since 5.8.38
*/
package cn.hutool.ai.model.grok;

View File

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

View File

@@ -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";
}
}

View File

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

View File

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

View File

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

View 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的封装实现
*
* @author elichow
* @since 5.8.39
*/
package cn.hutool.ai.model.hutool;

View File

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

View File

@@ -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";
}
}

View File

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

View File

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

View File

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

View 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.
*/
/**
* 对openai的封装实现
*
* @author elichow
* @since 5.8.38
*/
package cn.hutool.ai.model.openai;

View 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;

View 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;

View File

@@ -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

View File

@@ -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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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;

View File

@@ -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>

View File

@@ -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>

View File

@@ -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 {

View File

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

View File

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

View File

@@ -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>

View File

@@ -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>

View File

@@ -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方法时会

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

View File

@@ -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);
}
// 自定义值

View File

@@ -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);
}
// 自定义值

View File

@@ -31,7 +31,7 @@ import java.util.stream.Collectors;
* 日期时间工具类
*
* @author xiaoleilu
* @see LocalDateTimeUtil java8日工具类
* @see LocalDateTimeUtil java8日工具类
* @see DatePattern 日期常用格式工具类
*/
public class DateUtil extends CalendarUtil {

View File

@@ -14,7 +14,7 @@ import java.util.Date;
import java.util.TimeZone;
/**
* JDK8+中的{@link LocalDateTime} 工具类封装
* JDK8+中的{@link LocalDateTime}工具类封装
*
* @author looly
* @see DatePattern 常用格式工具类

View File

@@ -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), "地藏节");

View File

@@ -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, ()-&gt;{
* // 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");
}
/**
* 断言给定集合非空
* 并使用指定的函数获取错误信息返回

View File

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

View File

@@ -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);
/**

View File

@@ -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位身份证号码
*/

View File

@@ -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;
/**

View File

@@ -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;

View File

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

View File

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

View File

@@ -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}

View File

@@ -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;//
}
/**

View File

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

View File

@@ -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;

View File

@@ -153,7 +153,7 @@ public class ReflectUtil {
/**
* 获取指定类中字段名和字段对应的有序Map包括其父类中的字段<br>
* 如果子类与父类中存在同名字段,则这两个字段同时存在,子类字段在前,父类字段在后。
* 如果子类与父类中存在同名字段,则后者覆盖前者
*
* @param beanClass 类
* @return 字段名和字段对应的Map有序

View File

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

View File

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

View File

@@ -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

View File

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

View File

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

View File

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

View File

@@ -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