Compare commits

..

50 Commits

Author SHA1 Message Date
41a577fd00 fix: 为 SB4 AI 配置补充占位符默认值
为 AI 相关 API Key 和 Base URL 环境变量增加默认值,避免本地未配置环境变量时 Spring Boot 启动报错。

涉及:
- OpenAI / Anthropic base-url 默认值
- 各 AI 平台 api-key 默认 sk-xxxx
2026-06-29 21:48:21 -07:00
1150e5af02 Merge branch 'master-jdk17' of https://gitee.com/zhijiantianya/ruoyi-vue-pro into master-jdk25 2026-06-29 17:20:18 -07:00
ec3f7cbf73 v2026_06_发布:新增 IM 即时通讯,正式发布 Spring Boot 4.X 支持单体、微服务两种 2026-06-29 17:19:06 -07:00
c781012f61 v2026_06_发布:新增 IM 即时通讯,正式发布 Spring Boot 4.X 支持单体、微服务两种 2026-06-29 17:15:16 -07:00
0190d7c91c Merge branch 'master-jdk17' of https://gitee.com/zhijiantianya/ruoyi-vue-pro into master-jdk25
# Conflicts:
#	pom.xml
#	yudao-dependencies/pom.xml
2026-06-29 17:14:23 -07:00
7d02fe2f85 v2026_06_发布:新增 IM 即时通讯,正式发布 Spring Boot 4.X 支持单体、微服务两种 2026-06-29 16:59:43 -07:00
8fed626c0d !1570 feat: [bpm] 回退方法使用 moveActivityIdsToSingleActivityId 说明
Merge pull request !1570 from Jason/master-jdk17-bpm-bug-fix
2026-06-29 23:54:50 +00:00
b33920a938 feat(ai):模块按需开启 2026-06-29 16:53:21 -07:00
efd6c728aa feat: 接入 Grok 和阶跃星辰聊天模型
- 新增 StepFun 平台枚举、配置属性和 ChatModel 适配
- 接入 Grok 官方 API 地址和默认模型配置
- 为 Grok、StepFun 增加聊天模型集成测试
- 在默认 AI 配置中补充 Grok、StepFun 占位符配置
- 补充 StepFun 到聊天参数构建逻辑
2026-06-29 16:52:12 -07:00
fa66aca3cf refactor(ai): 更新混元和百川模型接入 2026-06-29 09:59:50 -07:00
74aab657c3 chore: 放开 AI、BPM 和 ERP 模块编译 2026-06-29 09:59:22 -07:00
74c88fd90e feat: 更新百度文心一言接入配置
- 将文心一言默认模型调整为 ernie-5.1
- 文心一言配置改用 YIYAN_API_KEY 占位符
- 文心一言测试改为读取 YIYAN_API_KEY / YIYAN_MODEL 环境变量
- 补充测试 API Key 校验并打印模型返回内容
- 更新文档为千帆 V2 API Key 接入方式
- 说明千帆 V2 不再需要 Secret Key 和创建应用
- 更新文档模型列表为 ERNIE 5.1、ERNIE 5.0、ERNIE X1.1
- 移除旧的百度创建应用截图
2026-06-29 09:14:16 -07:00
b3a8a8f158 feat: 更新豆包模型接入配置
- 将豆包默认模型调整为 doubao-seed-2-1-turbo-260628
- 豆包客户端改用 DeepSeekChatModel 兼容实现,避免运行时 ChatOptions 类型不匹配
- 配置豆包 Ark 接口的 /v3/chat/completions 路径
- 豆包测试改为读取 DOUBAO_API_KEY / DOUBAO_MODEL 环境变量
- 移除测试中的真实密钥和旧模型标识
- 复用当前模型验证豆包 thinking 流式调用
2026-06-29 08:28:09 -07:00
7caaf3e543 feat: 优化讯飞星火 X2 接入
- 放开 AI 模块相关依赖,启用 yudao-module-ai
- 将讯飞星火配置简化为单一 api-key,占位符读取 XINGHUO_API_KEY
- 重构 XingHuoChatModel,支持 x2 与 x2-flash 按模型路由到不同接口
- 基于 DeepSeekChatOptions 构建星火客户端,屏蔽内部 baseUrl 与 spark-x 适配逻辑
- 调整动态 ChatModel 缓存粒度,不再按模型标识创建重复客户端
- 更新 XingHuoChatModelTests,移除硬编码密钥并支持环境变量配置
2026-06-29 06:49:53 -07:00
2d3d539a46 feat(ai):完善 AI 模型配置和集成测试
- 放开 ai 模块编译配置
- 将智谱、MiniMax、月之暗面、通义千问、硅基流动配置改为环境变量占位符
- 更新硅基流动默认模型为 deepseek-ai/DeepSeek-V4-Pro
- 调整各平台 ChatModel 测试读取环境变量并补充 API Key 校验
- 清理通义千问、硅基流动测试中的硬编码密钥
2026-06-29 04:37:44 -07:00
aad44c2d60 feat(ai): 使用 Spring AI 官方 Gemini 接入
- 开放 AI 模块构建,并引入 spring-ai-starter-model-google-genai
- 移除自定义 GeminiChatModel,改用 GoogleGenAiChatModel
- 支持 Gemini 动态密钥和自定义 URL,并兼容 TeamOrouter Bearer 鉴权
- 将 OpenAI、Claude、Gemini 配置改为环境变量占位符
- 调整 OpenAI、Claude、Azure OpenAI、Gemini 测试用例
2026-06-28 22:54:46 -07:00
6dcd00f197 feat: 完善 AI 模块 Claude 接入测试
- 放开 bpm、erp、ai 模块及 yudao-server 对应依赖
- 调整 Anthropic 配置,改为 TeamOrouter base-url 并通过 ANTHROPIC_API_KEY 占位符读取密钥
- 新增 AiUtils.validateApiKey,避免集成测试使用默认占位 API Key 发起调用
- 调整 OpenAI、Azure OpenAI、Anthropic 集成测试,运行前校验 API Key
- 优化 Anthropic 测试输出,打印实际模型响应内容
- 为 Anthropic thinking 测试开启 thinkingEnabled 配置
2026-06-28 18:54:54 -07:00
6850555c19 feat(ai): 支持 AI 密钥占位符配置
- 放开 AI 模块及相关服务依赖,支持本地编译验证
- 支持 DB 动态配置中的 Spring 占位符解析,例如 ${OPENAI_API_KEY}
- 占位符解析失败时抛出业务异常,提示检查环境变量或配置项
- 调整 OpenAI 默认配置为环境变量 API Key
- 补充 OpenAI、Azure OpenAI 集成测试配置示例
2026-06-28 07:23:58 -07:00
137b30b72f fix: 补全 Azure OpenAI 向量模型接入
- 为 AiModelFactoryImpl 补回 AZURE_OPENAI EmbeddingModel 分支
- 基于 OpenAiEmbeddingModel 的 Azure 模式创建 Azure OpenAI 向量模型
- 使用 deploymentName(model) 适配 Azure OpenAI 部署名
2026-06-28 05:15:23 -07:00
0f10fcc0c4 Merge branch 'master-jdk17' of https://gitee.com/zhijiantianya/ruoyi-vue-pro into master-jdk25 2026-06-28 03:46:13 -07:00
d56607e0aa pom:revision 增加 jdk25 标识 2026-06-27 19:51:03 -07:00
6c643f57d1 README:增加 master-jdk25 & spring-boot-4 说明 2026-06-27 15:59:43 -07:00
a70c1cb1b4 Merge branch 'master-jdk17' of https://gitee.com/zhijiantianya/ruoyi-vue-pro into feature/jdk25 2026-06-27 15:45:57 -07:00
a763646e7f ignore 2026-06-27 15:24:49 -07:00
e3c9e7ebdd fix(iot):修复 IoT 网关独立打包启动依赖
- 将 IoT 模块 Kafka 依赖切换为 spring-boot-starter-kafka,适配 Spring Boot 4.1 的 Kafka 自动配置拆分
- 为 iot-gateway 补充 validation 与 Guava 运行时依赖,避免独立 fat jar 启动失败
- 验证 iot-gateway 可单独打包并使用 local 配置启动
2026-06-27 14:51:17 -07:00
a0084223ed fix(bpm):BpmHttpRequestUtils 注释错误 2026-06-27 14:30:16 -07:00
13a5631804 feat(iot):IotDeviceApiImpl 使用 RestTemplateBuilder 2026-06-27 10:17:14 -07:00
3550f099bd Merge branch 'master-jdk17' of https://gitee.com/zhijiantianya/ruoyi-vue-pro into feature/jdk25 2026-06-27 07:05:41 -07:00
38970d07b7 feat(ai): 升级 Spring AI 2.0 适配 AI 模块
- 升级 Spring AI 到 2.0.0、spring-ai-alibaba 到 2.0.0-M1.1
- 放开 AI 模块编译,并移除 Azure/ZhiPu/MiniMax/QianFan/Moonshot 旧 starter 依赖
- 将 YiYan、ZhiPu、MiniMax、Moonshot 调整为 OpenAI 兼容文本模型接入,并裁剪图片、向量能力
- 适配 Spring AI 2.0 的 options、getOptions、OpenAI/DeepSeek/Ollama 构造方式
- 手动创建 DashScope chat/image/embedding Bean,并全局排除暂不兼容的 DashScope 自动配置
- 补齐 spring-ai-alibaba 仍依赖的 ToolExecutionEligibilityPredicate shim
- 修复 Grok 默认 Bean 包装类型错误
- 调整 RedisVectorStore 通过 Redis 配置构造 RedisClient,避免依赖 JedisConnectionFactory
- 更新相关模型集成测试,并移除已裁剪能力的图片测试
2026-06-27 06:50:02 -07:00
4f3701b47c fix:兼容 PathPattern 模式的接口路径提取
- 兼容 PatternsCondition 和 PathPatternsCondition 两种路径条件
- 修复 @PermitAll 接口白名单路径提取逻辑
- 修复租户忽略 URL 的 PathPattern 提取逻辑
2026-06-26 18:14:04 -07:00
b17432dfa2 fix:优化 Jackson3 NumberSerializer 继承实现
- 继承 Jackson3 提供的 NumberSerializer
- 复用默认数值序列化逻辑
- 保留超出 JS 安全整数范围转字符串的处理
2026-06-26 17:57:04 -07:00
6bd737d291 Merge branch 'master-jdk17' of https://gitee.com/zhijiantianya/ruoyi-vue-pro into feature/jdk25
# Conflicts:
#	yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/json/JsonUtils.java
2026-06-26 17:27:20 -07:00
defab9fa3b fix:修正 spring boot 4.x encoding 配置项的变更 2026-06-26 17:12:39 -07:00
f08eefe1cf Merge branch 'master-jdk17' of https://gitee.com/zhijiantianya/ruoyi-vue-pro into feature/jdk25 2026-06-26 16:57:54 -07:00
501c658a3c fix(redis): 适配 Redisson 4.6 的 Spring Data Redis 依赖 2026-06-26 11:06:59 -07:00
78b4c814f7 Merge branch 'master-jdk17' of https://gitee.com/zhijiantianya/ruoyi-vue-pro into feature/jdk25
# Conflicts:
#	yudao-framework/yudao-spring-boot-starter-test/src/main/java/cn/iocoder/yudao/framework/test/core/ut/BaseDbAndRedisUnitTest.java
#	yudao-framework/yudao-spring-boot-starter-test/src/main/java/cn/iocoder/yudao/framework/test/core/ut/BaseRedisUnitTest.java
2026-06-26 11:03:07 -07:00
a7abc52f5e fix: 调整 Spring Boot 4 Redis 序列化器配置
- 改回使用 RedisSerializer.json() 构建 Redis value 序列化器
- 移除手动创建 GenericJacksonJsonRedisSerializer 和 DefaultTyping 配置
- 依赖 Spring Data Redis 4 + Jackson 3 默认支持 Java Time 类型,避免额外 ObjectMapper 配置带来兼容风险
2026-06-26 10:47:27 -07:00
6eac16b0a3 fix:easy-trans 在 spring boot 4.x 的兼容性问题 2026-06-26 09:29:52 -07:00
478a41c3f7 Merge branch 'master-jdk17' of https://gitee.com/zhijiantianya/ruoyi-vue-pro into feature/jdk25 2026-06-26 09:04:13 -07:00
6742cd0085 Merge branch 'master-jdk17' of https://gitee.com/zhijiantianya/ruoyi-vue-pro into feature/jdk25
# Conflicts:
#	pom.xml
#	yudao-dependencies/pom.xml
#	yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/translate/config/YudaoTranslateAutoConfiguration.java
#	yudao-server/src/main/resources/application-dev.yaml
#	yudao-server/src/main/resources/application-local.yaml
2026-06-26 08:55:47 -07:00
6ade5c35c6 chore(im): 同步 IM 模块并补全相关适配
- 打开 IM 同步后所需的业务模块编译链路
- 补齐 Jackson 3 相关类型与异常适配
2026-06-25 07:21:33 -07:00
da2021edd2 Merge branch 'master-jdk17' of https://gitee.com/zhijiantianya/ruoyi-vue-pro into feature/jdk25
# Conflicts:
#	yudao-dependencies/pom.xml
2026-06-25 05:30:36 -07:00
a60accfc2f chore: 升级 Spring Boot 4.1 并适配相关兼容性
- 升级 Spring Boot 4.1.0、Redisson 4.6.1、Flowable 8.0.0
- 移除不再需要的 spring-milestones 仓库和历史自动配置排除
- 兼容 Spring MVC PathPattern,修复租户忽略 URL 收集逻辑
- 为 Redis 序列化显式启用 DefaultTyping,避免缓存反序列化为 LinkedHashMap
- 重新开启 API 加密配置
2026-06-25 05:28:50 -07:00
1ef9ab67ae feat: 升级 easy-trans && 采用 HttpComponents(HttpClient 5)请求微信 2026-05-23 15:32:23 +08:00
7965212859 feat: 升级 easy-trans && 采用 HttpComponents(HttpClient 5)请求微信 2026-05-23 15:32:08 +08:00
cb3bebfbd8 feat: 升级 spring-ai v2 2026-05-23 15:11:07 +08:00
047196f762 feat: 升级Jackson3 2026-05-23 13:36:06 +08:00
72332d53d4 feat: com.fasterxml.jackson => tools.jackson 2026-05-23 13:05:12 +08:00
cd8f7a8ed8 feat: Redis JSON 序列化改成显式使用 Jackson2 的 GenericJackson2JsonRedisSerializer(JsonUtils.getObjectMapper()) 2026-05-23 12:22:44 +08:00
f41d61c654 chore: 升级Spring Boot 4.0 2026-05-23 12:17:32 +08:00
167 changed files with 2603 additions and 1682 deletions

View File

@ -1,5 +1,5 @@
<p align="center">
<img src="https://img.shields.io/badge/Spring%20Boot-3.5.15-blue.svg" alt="Downloads">
<img src="https://img.shields.io/badge/Spring%20Boot-4.1.0-blue.svg" alt="Downloads">
<img src="https://img.shields.io/badge/Vue-3.2-blue.svg" alt="Downloads">
<img src="https://img.shields.io/github/license/YunaiV/ruoyi-vue-pro" alt="Downloads" />
</p>
@ -350,26 +350,26 @@
| 框架 | 说明 | 版本 | 学习指南 |
|---------------------------------------------------------------------------------------------|------------------|----------------|----------------------------------------------------------------|
| [Spring Boot](https://spring.io/projects/spring-boot) | 应用开发框架 | 3.5.15 | [文档](https://github.com/YunaiV/SpringBoot-Labs) |
| [Spring Boot](https://spring.io/projects/spring-boot) | 应用开发框架 | 4.1.0 | [文档](https://github.com/YunaiV/SpringBoot-Labs) |
| [MySQL](https://www.mysql.com/cn/) | 数据库服务器 | 5.7 / 8.0+ | |
| [Druid](https://github.com/alibaba/druid) | JDBC 连接池、监控组件 | 1.2.28 | [文档](http://www.iocoder.cn/Spring-Boot/datasource-pool/?yudao) |
| [MyBatis Plus](https://mp.baomidou.com/) | MyBatis 增强工具包 | 3.5.16 | [文档](http://www.iocoder.cn/Spring-Boot/MyBatis/?yudao) |
| [Dynamic Datasource](https://dynamic-datasource.com/) | 动态数据源 | 4.5.0 | [文档](http://www.iocoder.cn/Spring-Boot/datasource-pool/?yudao) |
| [Redis](https://redis.io/) | key-value 数据库 | 5.0 / 6.0 /7.0 | |
| [Redisson](https://github.com/redisson/redisson) | Redis 客户端 | 4.6.1 | [文档](http://www.iocoder.cn/Spring-Boot/Redis/?yudao) |
| [Spring MVC](https://github.com/spring-projects/spring-framework/tree/master/spring-webmvc) | MVC 框架 | 6.2.19 | [文档](http://www.iocoder.cn/SpringMVC/MVC/?yudao) |
| [Spring Security](https://github.com/spring-projects/spring-security) | Spring 安全框架 | 6.5.11 | [文档](http://www.iocoder.cn/Spring-Boot/Spring-Security/?yudao) |
| [Hibernate Validator](https://github.com/hibernate/hibernate-validator) | 参数校验组件 | 8.0.3 | [文档](http://www.iocoder.cn/Spring-Boot/Validation/?yudao) |
| [Spring MVC](https://github.com/spring-projects/spring-framework/tree/master/spring-webmvc) | MVC 框架 | 7.0.8 | [文档](http://www.iocoder.cn/SpringMVC/MVC/?yudao) |
| [Spring Security](https://github.com/spring-projects/spring-security) | Spring 安全框架 | 7.1.0 | [文档](http://www.iocoder.cn/Spring-Boot/Spring-Security/?yudao) |
| [Hibernate Validator](https://github.com/hibernate/hibernate-validator) | 参数校验组件 | 9.1.0 | [文档](http://www.iocoder.cn/Spring-Boot/Validation/?yudao) |
| [Flowable](https://github.com/flowable/flowable-engine) | 工作流引擎 | 8.0.0 | [文档](https://doc.iocoder.cn/bpm/) |
| [Quartz](https://github.com/quartz-scheduler) | 任务调度组件 | 2.5.0 | [文档](http://www.iocoder.cn/Spring-Boot/Job/?yudao) |
| [Springdoc](https://springdoc.org/) | Swagger 文档 | 2.8.17 | [文档](http://www.iocoder.cn/Spring-Boot/Swagger/?yudao) |
| [Quartz](https://github.com/quartz-scheduler) | 任务调度组件 | 2.5.2 | [文档](http://www.iocoder.cn/Spring-Boot/Job/?yudao) |
| [Springdoc](https://springdoc.org/) | Swagger 文档 | 3.0.3 | [文档](http://www.iocoder.cn/Spring-Boot/Swagger/?yudao) |
| [SkyWalking](https://skywalking.apache.org/) | 分布式应用追踪系统 | 9.6.0 | [文档](http://www.iocoder.cn/Spring-Boot/SkyWalking/?yudao) |
| [Spring Boot Admin](https://github.com/codecentric/spring-boot-admin) | Spring Boot 监控平台 | 3.5.9 | [文档](http://www.iocoder.cn/Spring-Boot/Admin/?yudao) |
| [Jackson](https://github.com/FasterXML/jackson) | JSON 工具库 | 2.21.4 | |
| [Spring Boot Admin](https://github.com/codecentric/spring-boot-admin) | Spring Boot 监控平台 | 4.0.4 | [文档](http://www.iocoder.cn/Spring-Boot/Admin/?yudao) |
| [Jackson](https://github.com/FasterXML/jackson) | JSON 工具库 | 3.1.4 | |
| [MapStruct](https://mapstruct.org/) | Java Bean 转换 | 1.6.3 | [文档](http://www.iocoder.cn/Spring-Boot/MapStruct/?yudao) |
| [Lombok](https://projectlombok.org/) | 消除冗长的 Java 代码 | 1.18.46 | [文档](http://www.iocoder.cn/Spring-Boot/Lombok/?yudao) |
| [JUnit](https://junit.org/junit5/) | Java 单元测试框架 | 5.12.2 | - |
| [Mockito](https://github.com/mockito/mockito) | Java Mock 框架 | 5.17.0 | - |
| [JUnit](https://junit.org/junit5/) | Java 单元测试框架 | 6.0.3 | - |
| [Mockito](https://github.com/mockito/mockito) | Java Mock 框架 | 5.23.0 | - |
## 🐷 演示图

View File

@ -36,17 +36,17 @@
<url>https://github.com/YunaiV/ruoyi-vue-pro</url>
<properties>
<revision>2026.05-SNAPSHOT</revision>
<revision>2026.06-jdk25-SNAPSHOT</revision>
<!-- Maven 相关 -->
<java.version>17</java.version>
<java.version>25</java.version>
<maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.target>${java.version}</maven.compiler.target>
<maven-surefire-plugin.version>3.5.3</maven-surefire-plugin.version>
<maven-compiler-plugin.version>3.14.0</maven-compiler-plugin.version>
<flatten-maven-plugin.version>1.7.2</flatten-maven-plugin.version>
<!-- maven-surefire-plugin 暂时无法通过 bom 的依赖读取(兼容老版本 IDEA 2024 及以前版本) -->
<lombok.version>1.18.42</lombok.version>
<spring.boot.version>3.5.15</spring.boot.version>
<lombok.version>1.18.46</lombok.version>
<spring.boot.version>4.1.0</spring.boot.version>
<mapstruct.version>1.6.3</mapstruct.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

File diff suppressed because it is too large Load Diff

View File

@ -14,12 +14,12 @@
<url>https://github.com/YunaiV/ruoyi-vue-pro</url>
<properties>
<revision>2026.05-SNAPSHOT</revision>
<revision>2026.06-jdk25-SNAPSHOT</revision>
<flatten-maven-plugin.version>1.7.2</flatten-maven-plugin.version>
<!-- 统一依赖管理 -->
<spring.boot.version>3.5.15</spring.boot.version>
<spring.boot.version>4.1.0</spring.boot.version>
<!-- Web 相关 -->
<springdoc.version>2.8.17</springdoc.version>
<springdoc.version>3.0.3</springdoc.version>
<knife4j.version>4.5.0</knife4j.version>
<!-- DB 相关 -->
<druid.version>1.2.28</druid.version>
@ -39,7 +39,7 @@
<lock4j.version>2.2.7</lock4j.version>
<!-- 监控相关 -->
<skywalking.version>9.6.0</skywalking.version>
<spring-boot-admin.version>3.5.9</spring-boot-admin.version>
<spring-boot-admin.version>4.0.4</spring-boot-admin.version>
<opentracing.version>0.33.0</opentracing.version>
<!-- Test 测试相关 -->
<podam.version>8.0.2.RELEASE</podam.version>
@ -139,6 +139,17 @@
<version>${spring.boot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aspectj</artifactId>
<version>${spring.boot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-jackson</artifactId>
<version>${spring.boot.version}</version>
</dependency>
<!-- Web 相关 -->
<dependency>
<groupId>cn.iocoder.boot</groupId>
@ -184,7 +195,7 @@
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-3-starter</artifactId>
<artifactId>druid-spring-boot-4-starter</artifactId>
<version>${druid.version}</version>
</dependency>
<dependency>
@ -195,7 +206,7 @@
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<artifactId>mybatis-plus-spring-boot4-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<dependency>
@ -210,7 +221,7 @@
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot3-starter</artifactId> <!-- 多数据源 -->
<artifactId>dynamic-datasource-spring-boot4-starter</artifactId> <!-- 多数据源 -->
<version>${dynamic-datasource.version}</version>
</dependency>
<dependency>
@ -260,18 +271,8 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</exclusion>
<exclusion>
<groupId>org.redisson</groupId>
<!-- Redisson 4.6.x 默认依赖 redisson-spring-data-41适配 Spring Data Redis 4.x排除后使用下方的 spring-data-35 适配 Spring Boot 3.5 -->
<artifactId>redisson-spring-data-41</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-data-35</artifactId>
<version>${redisson.version}</version>
</dependency>
<dependency>
<groupId>com.dameng</groupId>

View File

@ -96,20 +96,15 @@
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<groupId>tools.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<scope>provided</scope> <!-- 设置为 provided只有工具类需要使用到 -->
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<groupId>tools.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<scope>provided</scope> <!-- 设置为 provided只有工具类需要使用到 -->
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<scope>provided</scope> <!-- 设置为 provided只有工具类需要使用到 -->
</dependency>
<dependency>
<groupId>org.slf4j</groupId>

View File

@ -6,20 +6,18 @@ import cn.hutool.json.JSONUtil;
import cn.iocoder.yudao.framework.common.util.json.databind.TimestampLocalDateTimeDeserializer;
import cn.iocoder.yudao.framework.common.util.json.databind.TimestampLocalDateTimeSerializer;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JacksonException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import lombok.Getter;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import tools.jackson.core.JacksonException;
import tools.jackson.core.type.TypeReference;
import tools.jackson.databind.DeserializationFeature;
import tools.jackson.databind.JsonNode;
import tools.jackson.databind.ObjectMapper;
import tools.jackson.databind.SerializationFeature;
import tools.jackson.databind.json.JsonMapper;
import tools.jackson.databind.module.SimpleModule;
import java.io.IOException;
import java.lang.reflect.Type;
import java.time.LocalDateTime;
import java.util.ArrayList;
@ -38,14 +36,14 @@ public class JsonUtils {
private static ObjectMapper objectMapper = buildObjectMapper();
private static ObjectMapper buildObjectMapper() {
SimpleModule simpleModule = new JavaTimeModule()
SimpleModule simpleModule = new SimpleModule()
// 解决 LocalDateTime 的序列化
.addSerializer(LocalDateTime.class, TimestampLocalDateTimeSerializer.INSTANCE)
.addDeserializer(LocalDateTime.class, TimestampLocalDateTimeDeserializer.INSTANCE);
return JsonMapper.builder()
.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS)
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
.defaultPropertyInclusion(JsonInclude.Value.construct(JsonInclude.Include.NON_NULL, JsonInclude.Include.NON_NULL))
.changeDefaultPropertyInclusion(value -> JsonInclude.Value.construct(JsonInclude.Include.NON_NULL, JsonInclude.Include.NON_NULL))
.addModule(simpleModule)
.build();
}
@ -120,7 +118,7 @@ public class JsonUtils {
}
try {
return objectMapper.readValue(text, objectMapper.getTypeFactory().constructType(type));
} catch (IOException e) {
} catch (JacksonException e) {
log.error("json parse err,json:{}", text, e);
throw new RuntimeException(e);
}
@ -148,7 +146,7 @@ public class JsonUtils {
}
try {
return objectMapper.readValue(bytes, clazz);
} catch (IOException e) {
} catch (JacksonException e) {
log.error("json parse err,json:{}", bytes, e);
throw new RuntimeException(e);
}
@ -251,7 +249,7 @@ public class JsonUtils {
public static JsonNode parseTree(byte[] text) {
try {
return objectMapper.readTree(text);
} catch (IOException e) {
} catch (JacksonException e) {
log.error("json parse err,json:{}", text, e);
throw new RuntimeException(e);
}

View File

@ -1,10 +1,9 @@
package cn.iocoder.yudao.framework.common.util.json.databind;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.annotation.JacksonStdImpl;
import java.io.IOException;
import tools.jackson.core.JacksonException;
import tools.jackson.core.JsonGenerator;
import tools.jackson.databind.SerializationContext;
import tools.jackson.databind.annotation.JacksonStdImpl;
/**
* Long 序列化规则
@ -14,7 +13,7 @@ import java.io.IOException;
* @author 星语
*/
@JacksonStdImpl
public class NumberSerializer extends com.fasterxml.jackson.databind.ser.std.NumberSerializer {
public class NumberSerializer extends tools.jackson.databind.ser.jdk.NumberSerializer {
private static final long MAX_SAFE_INTEGER = 9007199254740991L;
private static final long MIN_SAFE_INTEGER = -9007199254740991L;
@ -26,7 +25,7 @@ public class NumberSerializer extends com.fasterxml.jackson.databind.ser.std.Num
}
@Override
public void serialize(Number value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
public void serialize(Number value, JsonGenerator gen, SerializationContext serializers) throws JacksonException {
// 超出范围 序列化位字符串
if (value.longValue() > MIN_SAFE_INTEGER && value.longValue() < MAX_SAFE_INTEGER) {
super.serialize(value, gen, serializers);

View File

@ -1,10 +1,10 @@
package cn.iocoder.yudao.framework.common.util.json.databind;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import tools.jackson.core.JacksonException;
import tools.jackson.core.JsonParser;
import tools.jackson.databind.DeserializationContext;
import tools.jackson.databind.ValueDeserializer;
import java.io.IOException;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
@ -14,12 +14,12 @@ import java.time.ZoneId;
*
* @author 老五
*/
public class TimestampLocalDateTimeDeserializer extends JsonDeserializer<LocalDateTime> {
public class TimestampLocalDateTimeDeserializer extends ValueDeserializer<LocalDateTime> {
public static final TimestampLocalDateTimeDeserializer INSTANCE = new TimestampLocalDateTimeDeserializer();
@Override
public LocalDateTime deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
public LocalDateTime deserialize(JsonParser p, DeserializationContext ctxt) throws JacksonException {
// 将 Long 时间戳,转换为 LocalDateTime 对象
return LocalDateTime.ofInstant(Instant.ofEpochMilli(p.getValueAsLong()), ZoneId.systemDefault());
}

View File

@ -5,12 +5,12 @@ import cn.hutool.core.util.ReflectUtil;
import cn.hutool.core.util.StrUtil;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import lombok.extern.slf4j.Slf4j;
import tools.jackson.core.JacksonException;
import tools.jackson.core.JsonGenerator;
import tools.jackson.databind.SerializationContext;
import tools.jackson.databind.ser.std.StdScalarSerializer;
import java.io.IOException;
import java.lang.reflect.Field;
import java.time.LocalDateTime;
import java.time.ZoneId;
@ -25,18 +25,22 @@ import java.util.concurrent.ConcurrentHashMap;
* @author 老五
*/
@Slf4j
public class TimestampLocalDateTimeSerializer extends JsonSerializer<LocalDateTime> {
public class TimestampLocalDateTimeSerializer extends StdScalarSerializer<LocalDateTime> {
public static final TimestampLocalDateTimeSerializer INSTANCE = new TimestampLocalDateTimeSerializer();
private static final Map<Class<?>, Map<String, Field>> FIELD_CACHE = new ConcurrentHashMap<>();
public TimestampLocalDateTimeSerializer() {
super(LocalDateTime.class);
}
@Override
public void serialize(LocalDateTime value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
public void serialize(LocalDateTime value, JsonGenerator gen, SerializationContext serializers) throws JacksonException {
// 情况一:有 JsonFormat 自定义注解则使用它。https://github.com/YunaiV/ruoyi-vue-pro/pull/1019
String fieldName = gen.getOutputContext().getCurrentName();
String fieldName = gen.streamWriteContext().currentName();
if (fieldName != null) {
Object currentValue = gen.getOutputContext().getCurrentValue();
Object currentValue = gen.currentValue();
if (currentValue != null) {
Class<?> clazz = currentValue.getClass();
Map<String, Field> fieldMap = FIELD_CACHE.computeIfAbsent(clazz, this::buildFieldMap);

View File

@ -26,10 +26,9 @@ public class ServletUtils {
* @param response 响应
* @param object 对象,会序列化成 JSON 字符串
*/
@SuppressWarnings("deprecation") // 必须使用 APPLICATION_JSON_UTF8_VALUE否则会乱码
public static void writeJSON(HttpServletResponse response, Object object) {
String content = JsonUtils.toJsonString(object);
JakartaServletUtil.write(response, content, MediaType.APPLICATION_JSON_UTF8_VALUE);
JakartaServletUtil.write(response, content, MediaType.APPLICATION_JSON_VALUE + ";charset=UTF-8");
}
/**

View File

@ -129,6 +129,7 @@ public class YudaoTenantAutoConfiguration {
*
* @return 忽略租户的 URL 集合
*/
@SuppressWarnings("removal")
private Set<String> getTenantIgnoreUrls() {
Set<String> ignoreUrls = new HashSet<>();
// 获得接口对应的 HandlerMethod 集合

View File

@ -2,8 +2,8 @@ package cn.iocoder.yudao.framework.tenant.core.mq.kafka;
import cn.hutool.core.util.StrUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.EnvironmentPostProcessor;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.env.EnvironmentPostProcessor;
import org.springframework.core.env.ConfigurableEnvironment;
/**

View File

@ -27,7 +27,7 @@ import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.MethodParameter;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.core.ResolvableType;
import org.springframework.lang.Nullable;
import org.jspecify.annotations.Nullable;
import org.springframework.messaging.Message;
import org.springframework.messaging.handler.HandlerMethod;
import org.springframework.util.ObjectUtils;

View File

@ -1,2 +1,2 @@
org.springframework.boot.env.EnvironmentPostProcessor=\
org.springframework.boot.EnvironmentPostProcessor=\
cn.iocoder.yudao.framework.tenant.core.mq.kafka.TenantKafkaEnvironmentPostProcessor

View File

@ -24,7 +24,7 @@
<!-- Spring 核心 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
<artifactId>spring-boot-starter-aspectj</artifactId>
</dependency>
<!-- Web 相关 -->

View File

@ -2,7 +2,7 @@ package cn.iocoder.yudao.framework.tracer.config;
import io.micrometer.core.instrument.MeterRegistry;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.actuate.autoconfigure.metrics.MeterRegistryCustomizer;
import org.springframework.boot.micrometer.metrics.autoconfigure.MeterRegistryCustomizer;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;

View File

@ -1,7 +1,7 @@
package cn.iocoder.yudao.framework.mq.rabbitmq.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.JacksonJsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
@ -18,11 +18,11 @@ import org.springframework.context.annotation.Bean;
public class YudaoRabbitMQAutoConfiguration {
/**
* Jackson2JsonMessageConverter Bean使用 jackson 序列化消息
* JacksonJsonMessageConverter Bean使用 jackson 序列化消息
*/
@Bean
public MessageConverter createMessageConverter() {
return new Jackson2JsonMessageConverter();
return new JacksonJsonMessageConverter();
}
}

View File

@ -71,11 +71,11 @@
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-3-starter</artifactId>
<artifactId>druid-spring-boot-4-starter</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<artifactId>mybatis-plus-spring-boot4-starter</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
@ -83,13 +83,7 @@
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot3-starter</artifactId> <!-- 多数据源 -->
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-undertow</artifactId>
</exclusion>
</exclusions>
<artifactId>dynamic-datasource-spring-boot4-starter</artifactId> <!-- 多数据源 -->
</dependency>
<dependency>

View File

@ -1,7 +1,7 @@
package cn.iocoder.yudao.framework.datasource.config;
import cn.iocoder.yudao.framework.datasource.core.filter.DruidAdRemoveFilter;
import com.alibaba.druid.spring.boot3.autoconfigure.properties.DruidStatProperties;
import com.alibaba.druid.spring.boot4.autoconfigure.properties.DruidStatProperties;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;

View File

@ -6,8 +6,8 @@ import cn.iocoder.yudao.framework.mybatis.core.util.JdbcUtils;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.annotation.IdType;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.EnvironmentPostProcessor;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.env.EnvironmentPostProcessor;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MapPropertySource;

View File

@ -6,16 +6,14 @@ import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.framework.mybatis.core.handler.DefaultDBFieldHandler;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration;
import com.baomidou.mybatisplus.core.handlers.IJsonTypeHandler;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import com.baomidou.mybatisplus.core.incrementer.IKeyGenerator;
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
import com.baomidou.mybatisplus.extension.handlers.Jackson3TypeHandler;
import com.baomidou.mybatisplus.extension.incrementer.*;
import com.baomidou.mybatisplus.extension.parser.JsqlParserGlobal;
import com.baomidou.mybatisplus.extension.parser.cache.JdkSerialCaffeineJsqlParseCache;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.ibatis.annotations.Mapper;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.autoconfigure.AutoConfiguration;
@ -25,6 +23,7 @@ import org.springframework.core.env.ConfigurableEnvironment;
import java.util.List;
import java.util.concurrent.TimeUnit;
import tools.jackson.databind.ObjectMapper;
/**
* MyBaits 配置类
@ -81,15 +80,15 @@ public class YudaoMybatisAutoConfiguration {
throw new IllegalArgumentException(StrUtil.format("DbType{} 找不到合适的 IKeyGenerator 实现类", dbType));
}
@Bean // 特殊:返回结果使用 Object 而不用 JacksonTypeHandler 的原因,避免因为 JacksonTypeHandler 被 mybatis 全局使用!
@Bean // 特殊:返回结果使用 Object 而不用 Jackson3TypeHandler 的原因,避免因为 Jackson3TypeHandler 被 mybatis 全局使用!
public Object jacksonTypeHandler(List<ObjectMapper> objectMappers) {
// 特殊:设置 JacksonTypeHandler 的 ObjectMapper
// 特殊:设置 Jackson3TypeHandler 的 ObjectMapper
ObjectMapper objectMapper = CollUtil.getFirst(objectMappers);
if (objectMapper == null) {
objectMapper = JsonUtils.getObjectMapper();
}
JacksonTypeHandler.setObjectMapper(objectMapper);
return new JacksonTypeHandler(Object.class);
Jackson3TypeHandler.setObjectMapper(objectMapper);
return new Jackson3TypeHandler(Object.class);
}
}

View File

@ -1,2 +1,2 @@
org.springframework.boot.env.EnvironmentPostProcessor=\
org.springframework.boot.EnvironmentPostProcessor=\
cn.iocoder.yudao.framework.mybatis.config.IdTypeEnvironmentPostProcessor

View File

@ -26,10 +26,6 @@
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-data-35</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
@ -37,8 +33,8 @@
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-jackson</artifactId>
</dependency>
</dependencies>

View File

@ -3,7 +3,7 @@ package cn.iocoder.yudao.framework.redis.config;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.redis.core.TimeoutRedisCacheManager;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.cache.CacheProperties;
import org.springframework.boot.cache.autoconfigure.CacheProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;

View File

@ -1,9 +1,6 @@
package cn.iocoder.yudao.framework.redis.config;
import cn.hutool.core.util.ReflectUtil;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.redisson.spring.starter.RedissonAutoConfigurationV2;
import org.redisson.spring.starter.RedissonAutoConfigurationV4;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.connection.RedisConnectionFactory;
@ -13,7 +10,7 @@ import org.springframework.data.redis.serializer.RedisSerializer;
/**
* Redis 配置类
*/
@AutoConfiguration(before = RedissonAutoConfigurationV2.class) // 目的:使用自己定义的 RedisTemplate Bean
@AutoConfiguration(before = RedissonAutoConfigurationV4.class) // 目的:使用自己定义的 RedisTemplate Bean
public class YudaoRedisAutoConfiguration {
/**
@ -35,11 +32,11 @@ public class YudaoRedisAutoConfiguration {
return template;
}
@SuppressWarnings("UnnecessaryLocalVariable")
public static RedisSerializer<?> buildRedisSerializer() {
RedisSerializer<Object> json = RedisSerializer.json();
// 解决 LocalDateTime 的序列化
ObjectMapper objectMapper = (ObjectMapper) ReflectUtil.getFieldValue(json, "mapper");
objectMapper.registerModules(new JavaTimeModule());
// 特殊spring boot 4.x 无需解决 LocalDateTime 的序列化
// 原因Spring Data Redis 4 使用 Jackson 3RedisSerializer.json() 已支持 Java Time 类型
return json;
}

View File

@ -27,7 +27,7 @@
<!-- Spring 核心 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
<artifactId>spring-boot-starter-aspectj</artifactId>
</dependency>
<!-- Web 相关 -->

View File

@ -156,6 +156,7 @@ public class YudaoWebSecurityConfigurerAdapter {
return webProperties.getAppApi().getPrefix() + url;
}
@SuppressWarnings("removal")
private Multimap<HttpMethod, String> getPermitAllUrlsFromAnnotations() {
Multimap<HttpMethod, String> result = HashMultimap.create();
// 获得接口对应的 HandlerMethod 集合

View File

@ -5,7 +5,7 @@ import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.security.core.LoginUser;
import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils;
import org.springframework.lang.Nullable;
import org.jspecify.annotations.Nullable;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;

View File

@ -1,7 +1,7 @@
package cn.iocoder.yudao.framework.test.config;
import com.github.fppt.jedismock.RedisServer;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.boot.data.redis.autoconfigure.DataRedisProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@ -16,14 +16,14 @@ import java.io.IOException;
*/
@Configuration(proxyBeanMethods = false)
@Lazy(false) // 禁止延迟加载
@EnableConfigurationProperties(RedisProperties.class)
@EnableConfigurationProperties(DataRedisProperties.class)
public class RedisTestConfiguration {
/**
* 创建模拟的 Redis Server 服务器
*/
@Bean
public RedisServer redisServer(RedisProperties properties) throws IOException {
public RedisServer redisServer(DataRedisProperties properties) throws IOException {
RedisServer redisServer = new RedisServer(properties.getPort());
// 一次执行多个单元测试时,貌似创建多个 spring 容器,导致不进行 stop。这样就导致端口被占用无法启动。。。
try {

View File

@ -3,7 +3,7 @@ package cn.iocoder.yudao.framework.test.config;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate;
import org.springframework.boot.autoconfigure.sql.init.SqlInitializationProperties;
import org.springframework.boot.sql.autoconfigure.init.SqlInitializationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.jdbc.init.DataSourceScriptDatabaseInitializer;
import org.springframework.boot.sql.init.AbstractScriptDatabaseInitializer;
@ -17,7 +17,7 @@ import javax.sql.DataSource;
/**
* SQL 初始化的测试 Configuration
*
* 为什么不使用 org.springframework.boot.autoconfigure.sql.init.DataSourceInitializationConfiguration 呢?
* 为什么不使用 org.springframework.boot.sql.autoconfigure.init.DataSourceInitializationConfiguration 呢?
* 因为我们在单元测试会使用 spring.main.lazy-initialization 为 true开启延迟加载。此时会导致 DataSourceInitializationConfiguration 初始化
* 不过呢,当前类的实现代码,基本是复制 DataSourceInitializationConfiguration 的哈!
*

View File

@ -6,12 +6,12 @@ import cn.iocoder.yudao.framework.mybatis.config.YudaoMybatisAutoConfiguration;
import cn.iocoder.yudao.framework.redis.config.YudaoRedisAutoConfiguration;
import cn.iocoder.yudao.framework.test.config.RedisTestConfiguration;
import cn.iocoder.yudao.framework.test.config.SqlInitializationTestConfiguration;
import com.alibaba.druid.spring.boot3.autoconfigure.DruidDataSourceAutoConfigure;
import com.alibaba.druid.spring.boot4.autoconfigure.DruidDataSourceAutoConfigure;
import com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration;
import org.redisson.spring.starter.RedissonAutoConfigurationV2;
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration;
import org.redisson.spring.starter.RedissonAutoConfigurationV4;
import org.springframework.boot.data.redis.autoconfigure.DataRedisAutoConfiguration;
import org.springframework.boot.jdbc.autoconfigure.DataSourceAutoConfiguration;
import org.springframework.boot.jdbc.autoconfigure.DataSourceTransactionManagerAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.ActiveProfiles;
@ -43,8 +43,8 @@ public class BaseDbAndRedisUnitTest {
// Redis 配置类
RedisTestConfiguration.class, // Redis 测试配置类,用于启动 RedisServer
YudaoRedisAutoConfiguration.class, // 自己的 Redis 配置类
RedisAutoConfiguration.class, // Spring Redis 自动配置类
RedissonAutoConfigurationV2.class, // Redisson 自动配置类
DataRedisAutoConfiguration.class, // Spring Redis 自动配置类
RedissonAutoConfigurationV4.class, // Redisson 自动配置类
// 其它配置类
SpringUtil.class

View File

@ -4,11 +4,11 @@ import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.framework.datasource.config.YudaoDataSourceAutoConfiguration;
import cn.iocoder.yudao.framework.mybatis.config.YudaoMybatisAutoConfiguration;
import cn.iocoder.yudao.framework.test.config.SqlInitializationTestConfiguration;
import com.alibaba.druid.spring.boot3.autoconfigure.DruidDataSourceAutoConfigure;
import com.alibaba.druid.spring.boot4.autoconfigure.DruidDataSourceAutoConfigure;
import com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration;
import com.github.yulichang.autoconfigure.MybatisPlusJoinAutoConfiguration;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration;
import org.springframework.boot.jdbc.autoconfigure.DataSourceAutoConfiguration;
import org.springframework.boot.jdbc.autoconfigure.DataSourceTransactionManagerAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.ActiveProfiles;

View File

@ -3,8 +3,8 @@ package cn.iocoder.yudao.framework.test.core.ut;
import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.framework.redis.config.YudaoRedisAutoConfiguration;
import cn.iocoder.yudao.framework.test.config.RedisTestConfiguration;
import org.redisson.spring.starter.RedissonAutoConfigurationV2;
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
import org.redisson.spring.starter.RedissonAutoConfigurationV4;
import org.springframework.boot.data.redis.autoconfigure.DataRedisAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.ActiveProfiles;
@ -23,9 +23,9 @@ public class BaseRedisUnitTest {
@Import({
// Redis 配置类
RedisTestConfiguration.class, // Redis 测试配置类,用于启动 RedisServer
RedisAutoConfiguration.class, // Spring Redis 自动配置类
DataRedisAutoConfiguration.class, // Spring Redis 自动配置类
YudaoRedisAutoConfiguration.class, // 自己的 Redis 配置类
RedissonAutoConfigurationV2.class, // Redisson 自动配置类
RedissonAutoConfigurationV4.class, // Redisson 自动配置类
// 其它配置类
SpringUtil.class

View File

@ -26,6 +26,14 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-jackson</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-restclient</artifactId>
</dependency>
<!-- spring boot 配置所需依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>

View File

@ -19,7 +19,6 @@ import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;
import cn.iocoder.yudao.framework.web.config.WebProperties;
import cn.iocoder.yudao.framework.web.core.filter.ApiRequestFilter;
import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils;
import com.fasterxml.jackson.databind.JsonNode;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.FilterChain;
@ -29,6 +28,7 @@ import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.method.HandlerMethod;
import tools.jackson.databind.JsonNode;
import java.io.IOException;
import java.time.LocalDateTime;

View File

@ -3,7 +3,7 @@ package cn.iocoder.yudao.framework.desensitize.core.base.annotation;
import cn.iocoder.yudao.framework.desensitize.core.base.handler.DesensitizationHandler;
import cn.iocoder.yudao.framework.desensitize.core.base.serializer.StringDesensitizeSerializer;
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import tools.jackson.databind.annotation.JsonSerialize;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;

View File

@ -7,16 +7,15 @@ import cn.hutool.core.util.ReflectUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.desensitize.core.base.annotation.DesensitizeBy;
import cn.iocoder.yudao.framework.desensitize.core.base.handler.DesensitizationHandler;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.BeanProperty;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.ContextualSerializer;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
import lombok.Getter;
import lombok.Setter;
import tools.jackson.core.JacksonException;
import tools.jackson.core.JsonGenerator;
import tools.jackson.databind.BeanProperty;
import tools.jackson.databind.SerializationContext;
import tools.jackson.databind.ValueSerializer;
import tools.jackson.databind.ser.std.StdSerializer;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
@ -28,7 +27,7 @@ import java.lang.reflect.Field;
* @author gaibu
*/
@SuppressWarnings("rawtypes")
public class StringDesensitizeSerializer extends StdSerializer<String> implements ContextualSerializer {
public class StringDesensitizeSerializer extends StdSerializer<String> {
@Getter
@Setter
@ -39,7 +38,7 @@ public class StringDesensitizeSerializer extends StdSerializer<String> implement
}
@Override
public JsonSerializer<?> createContextual(SerializerProvider serializerProvider, BeanProperty beanProperty) {
public ValueSerializer<?> createContextual(SerializationContext serializerProvider, BeanProperty beanProperty) {
DesensitizeBy annotation = beanProperty.getAnnotation(DesensitizeBy.class);
if (annotation == null) {
return this;
@ -52,7 +51,7 @@ public class StringDesensitizeSerializer extends StdSerializer<String> implement
@Override
@SuppressWarnings("unchecked")
public void serialize(String value, JsonGenerator gen, SerializerProvider serializerProvider) throws IOException {
public void serialize(String value, JsonGenerator gen, SerializationContext serializerProvider) throws JacksonException {
if (StrUtil.isBlank(value)) {
gen.writeNull();
return;
@ -83,7 +82,7 @@ public class StringDesensitizeSerializer extends StdSerializer<String> implement
* @return 字段
*/
private Field getField(JsonGenerator generator) {
String currentName = generator.getOutputContext().getCurrentName();
String currentName = generator.streamWriteContext().currentName();
Object currentValue = generator.currentValue();
Class<?> currentValueClass = currentValue.getClass();
return ReflectUtil.getField(currentValueClass, currentName);

View File

@ -4,18 +4,18 @@ import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.framework.common.util.json.databind.NumberSerializer;
import cn.iocoder.yudao.framework.common.util.json.databind.TimestampLocalDateTimeDeserializer;
import cn.iocoder.yudao.framework.common.util.json.databind.TimestampLocalDateTimeSerializer;
import com.fasterxml.jackson.databind.Module;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
import org.springframework.boot.jackson.autoconfigure.JacksonAutoConfiguration;
import org.springframework.boot.jackson.autoconfigure.JsonMapperBuilderCustomizer;
import org.springframework.context.annotation.Bean;
import tools.jackson.databind.JacksonModule;
import tools.jackson.databind.ObjectMapper;
import tools.jackson.databind.ext.javatime.deser.LocalDateDeserializer;
import tools.jackson.databind.ext.javatime.deser.LocalTimeDeserializer;
import tools.jackson.databind.ext.javatime.ser.LocalDateSerializer;
import tools.jackson.databind.ext.javatime.ser.LocalTimeSerializer;
import tools.jackson.databind.module.SimpleModule;
import java.time.LocalDate;
import java.time.LocalDateTime;
@ -29,26 +29,15 @@ public class YudaoJacksonAutoConfiguration {
* 从 Builder 源头定制(关键:使用 *ByType避免 handledType 要求)
*/
@Bean
public Jackson2ObjectMapperBuilderCustomizer ldtEpochMillisCustomizer() {
return builder -> builder
// Long -> Number
.serializerByType(Long.class, NumberSerializer.INSTANCE)
.serializerByType(Long.TYPE, NumberSerializer.INSTANCE)
// LocalDate / LocalTime
.serializerByType(LocalDate.class, LocalDateSerializer.INSTANCE)
.deserializerByType(LocalDate.class, LocalDateDeserializer.INSTANCE)
.serializerByType(LocalTime.class, LocalTimeSerializer.INSTANCE)
.deserializerByType(LocalTime.class, LocalTimeDeserializer.INSTANCE)
// LocalDateTime < - > EpochMillis
.serializerByType(LocalDateTime.class, TimestampLocalDateTimeSerializer.INSTANCE)
.deserializerByType(LocalDateTime.class, TimestampLocalDateTimeDeserializer.INSTANCE);
public JsonMapperBuilderCustomizer ldtEpochMillisCustomizer(JacksonModule timestampSupportModuleBean) {
return builder -> builder.addModule(timestampSupportModuleBean);
}
/**
* 以 Bean 形式暴露 ModuleBoot 会自动注册到所有 ObjectMapper
*/
@Bean
public Module timestampSupportModuleBean() {
public JacksonModule timestampSupportModuleBean() {
SimpleModule m = new SimpleModule("TimestampSupportModule");
// Long -> Number避免前端精度丢失
m.addSerializer(Long.class, NumberSerializer.INSTANCE);

View File

@ -14,10 +14,9 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration;
import org.springframework.boot.autoconfigure.web.servlet.WebMvcRegistrations;
import org.springframework.boot.webmvc.autoconfigure.WebMvcRegistrations;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.boot.restclient.RestTemplateBuilder;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.core.annotation.Order;
@ -144,7 +143,7 @@ public class YudaoWebAutoConfiguration {
/**
* 创建 RestTemplate 实例
*
* @param restTemplateBuilder {@link RestTemplateAutoConfiguration#restTemplateBuilder}
* @param restTemplateBuilder {@link RestTemplateBuilder#build}
*/
@Bean
@ConditionalOnMissingBean

View File

@ -15,7 +15,6 @@ import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.framework.common.util.monitor.TracerUtils;
import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;
import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils;
import com.fasterxml.jackson.databind.exc.InvalidFormatException;
import com.google.common.util.concurrent.UncheckedExecutionException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.ConstraintViolation;
@ -39,6 +38,7 @@ import org.springframework.web.method.annotation.MethodArgumentTypeMismatchExcep
import org.springframework.web.multipart.MaxUploadSizeExceededException;
import org.springframework.web.servlet.NoHandlerFoundException;
import org.springframework.web.servlet.resource.NoResourceFoundException;
import tools.jackson.databind.exc.InvalidFormatException;
import java.time.LocalDateTime;
import java.util.List;

View File

@ -9,7 +9,7 @@ import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
import org.springframework.boot.jackson.autoconfigure.JsonMapperBuilderCustomizer;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
@ -37,17 +37,17 @@ public class YudaoXssAutoConfiguration implements WebMvcConfigurer {
/**
* 注册 Jackson 的序列化器,用于处理 json 类型参数的 xss 过滤
*
* @return Jackson2ObjectMapperBuilderCustomizer
* @return JsonMapperBuilderCustomizer
*/
@Bean
@ConditionalOnMissingBean(name = "xssJacksonCustomizer")
@ConditionalOnProperty(value = "yudao.xss.enable", havingValue = "true")
public Jackson2ObjectMapperBuilderCustomizer xssJacksonCustomizer(XssProperties properties,
PathMatcher pathMatcher,
XssCleaner xssCleaner) {
public JsonMapperBuilderCustomizer xssJacksonCustomizer(XssProperties properties,
PathMatcher pathMatcher,
XssCleaner xssCleaner) {
// 在反序列化时进行 xss 过滤,可以替换使用 XssStringJsonSerializer在序列化时进行处理
return builder ->
builder.deserializerByType(String.class, new XssStringJsonDeserializer(properties, pathMatcher, xssCleaner));
return builder -> builder.addModule(new tools.jackson.databind.module.SimpleModule("XssStringModule")
.addDeserializer(String.class, new XssStringJsonDeserializer(properties, pathMatcher, xssCleaner)));
}
/**

View File

@ -3,16 +3,15 @@ package cn.iocoder.yudao.framework.xss.core.json;
import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;
import cn.iocoder.yudao.framework.xss.config.XssProperties;
import cn.iocoder.yudao.framework.xss.core.clean.XssCleaner;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.deser.std.StringDeserializer;
import jakarta.servlet.http.HttpServletRequest;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.PathMatcher;
import java.io.IOException;
import tools.jackson.core.JacksonException;
import tools.jackson.core.JsonParser;
import tools.jackson.core.JsonToken;
import tools.jackson.databind.DeserializationContext;
import tools.jackson.databind.deser.jdk.StringDeserializer;
/**
* XSS 过滤 jackson 反序列化器。
@ -36,19 +35,19 @@ public class XssStringJsonDeserializer extends StringDeserializer {
private final XssCleaner xssCleaner;
@Override
public String deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
public String deserialize(JsonParser p, DeserializationContext ctxt) throws JacksonException {
// 1. 白名单 URL 的处理
HttpServletRequest request = ServletUtils.getRequest();
if (request != null) {
String uri = ServletUtils.getRequest().getRequestURI();
if (properties.getExcludeUrls().stream().anyMatch(excludeUrl -> pathMatcher.match(excludeUrl, uri))) {
return p.getText();
return p.getString();
}
}
// 2. 真正使用 xssCleaner 进行过滤
if (p.hasToken(JsonToken.VALUE_STRING)) {
return xssCleaner.clean(p.getText());
return xssCleaner.clean(p.getString());
}
JsonToken t = p.currentToken();
// [databind#381]

View File

@ -19,9 +19,9 @@
国外OpenAI、Ollama、Midjourney、StableDiffusion、Suno
</description>
<properties>
<spring-ai.version>1.1.5</spring-ai.version>
<spring-ai.version>2.0.0</spring-ai.version>
<!-- https://mvnrepository.com/artifact/com.alibaba.cloud.ai/spring-ai-alibaba -->
<alibaba-ai.version>1.1.2.2</alibaba-ai.version>
<alibaba-ai.version>2.0.0-M1.1</alibaba-ai.version>
<tinyflow.version>1.2.6</tinyflow.version>
</properties>
@ -86,11 +86,6 @@
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-azure-openai</artifactId>
<version>${spring-ai.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-anthropic</artifactId>
@ -101,6 +96,11 @@
<artifactId>spring-ai-starter-model-deepseek</artifactId>
<version>${spring-ai.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-google-genai</artifactId>
<version>${spring-ai.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-ollama</artifactId>
@ -111,18 +111,6 @@
<artifactId>spring-ai-starter-model-stability-ai</artifactId>
<version>${spring-ai.version}</version>
</dependency>
<dependency>
<!-- 智谱 GLM -->
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-zhipuai</artifactId>
<version>${spring-ai.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-minimax</artifactId>
<version>${spring-ai.version}</version>
</dependency>
<dependency>
<!-- 通义千问 -->
<groupId>com.alibaba.cloud.ai</groupId>
@ -130,19 +118,6 @@
<version>${alibaba-ai.version}</version>
</dependency>
<dependency>
<!-- 文心一言 -->
<groupId>org.springaicommunity</groupId>
<artifactId>qianfan-spring-boot-starter</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<!-- 月之暗面 -->
<groupId>org.springaicommunity</groupId>
<artifactId>moonshot-spring-boot-starter</artifactId>
<version>1.0.0</version>
</dependency>
<!-- 向量存储https://db-engines.com/en/ranking/vector+dbms -->
<dependency>
<!-- Qdranthttps://qdrant.tech/ -->
@ -211,12 +186,22 @@
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-mcp-server-common</artifactId>
<version>${spring-ai.version}</version>
</dependency>
<dependency>
<!-- 客户端 -->
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-client</artifactId>
<version>${spring-ai.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-mcp-client-common</artifactId>
<version>${spring-ai.version}</version>
</dependency>
<!-- TinyFlowAI 工作流 -->
<dependency>
@ -260,4 +245,4 @@
</dependency>
</dependencies>
</project>
</project>

View File

@ -64,4 +64,4 @@ public class AiChatRoleRespVO implements VO {
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDateTime createTime;
}
}

View File

@ -11,7 +11,7 @@ import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
import com.baomidou.mybatisplus.extension.handlers.Jackson3TypeHandler;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
@ -114,7 +114,7 @@ public class AiChatMessageDO extends BaseDO {
/**
* 联网搜索的网页内容数组
*/
@TableField(typeHandler = JacksonTypeHandler.class)
@TableField(typeHandler = Jackson3TypeHandler.class)
private List<AiWebSearchResponse.WebPage> webSearchPages;
/**

View File

@ -10,7 +10,7 @@ import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
import com.baomidou.mybatisplus.extension.handlers.Jackson3TypeHandler;
import lombok.Data;
import org.springframework.ai.openai.OpenAiImageOptions;
import org.springframework.ai.stabilityai.api.StabilityAiImageOptions;
@ -107,13 +107,13 @@ public class AiImageDO extends BaseDO {
* 1. {@link OpenAiImageOptions}
* 2. {@link StabilityAiImageOptions}
*/
@TableField(typeHandler = JacksonTypeHandler.class)
@TableField(typeHandler = Jackson3TypeHandler.class)
private Map<String, Object> options;
/**
* mj buttons 按钮
*/
@TableField(typeHandler = JacksonTypeHandler.class)
@TableField(typeHandler = Jackson3TypeHandler.class)
private List<MidjourneyApi.Button> buttons;
/**

View File

@ -8,7 +8,7 @@ import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
import com.baomidou.mybatisplus.extension.handlers.Jackson3TypeHandler;
import lombok.Data;
import java.util.List;
@ -93,7 +93,7 @@ public class AiMusicDO extends BaseDO {
/**
* 音乐风格标签
*/
@TableField(typeHandler = JacksonTypeHandler.class)
@TableField(typeHandler = Jackson3TypeHandler.class)
private List<String> tags;
/**

View File

@ -12,6 +12,7 @@ public interface ErrorCodeConstants {
// ========== API 密钥 1-040-000-000 ==========
ErrorCode API_KEY_NOT_EXISTS = new ErrorCode(1_040_000_000, "API 密钥不存在");
ErrorCode API_KEY_DISABLE = new ErrorCode(1_040_000_001, "API 密钥已禁用!");
ErrorCode API_CONFIG_PLACEHOLDER_NOT_RESOLVED = new ErrorCode(1_040_000_002, "AI 配置({})无法解析,请检查环境变量或配置项");
// ========== API 模型 1-040-001-000 ==========
ErrorCode MODEL_NOT_EXISTS = new ErrorCode(1_040_001_000, "模型不存在!");

View File

@ -28,6 +28,7 @@ public enum AiPlatformEnum implements ArrayValuable<String> {
MINI_MAX("MiniMax", "MiniMax"), // 稀宇科技
MOONSHOT("Moonshot", "月之暗面"), // KIMI
BAI_CHUAN("BaiChuan", "百川智能"), // 百川智能
STEP_FUN("StepFun", "阶跃星辰"), // 阶跃星辰
// ========== 国外平台 ==========
@ -40,7 +41,7 @@ public enum AiPlatformEnum implements ArrayValuable<String> {
STABLE_DIFFUSION("StableDiffusion", "StableDiffusion"), // Stability AI
MIDJOURNEY("Midjourney", "Midjourney"), // Midjourney
SUNO("Suno", "Suno"), // Suno AI
GROK("Grok","Grok"), // Grok
GROK("Grok", "Grok"), // Grok
;

View File

@ -6,29 +6,42 @@ import cn.iocoder.yudao.module.ai.framework.ai.core.model.AiModelFactory;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.AiModelFactoryImpl;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.baichuan.BaiChuanChatModel;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.doubao.DouBaoChatModel;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.gemini.GeminiChatModel;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.grok.GrokChatModel;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.hunyuan.HunYuanChatModel;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.midjourney.api.MidjourneyApi;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.minimax.MiniMaxChatModel;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.moonshot.MoonshotChatModel;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.siliconflow.SiliconFlowApiConstants;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.siliconflow.SiliconFlowChatModel;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.stepfun.StepFunChatModel;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.suno.api.SunoApi;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.xinghuo.XingHuoChatModel;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.yiyan.YiYanChatModel;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.zhipu.ZhiPuChatModel;
import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.AiWebSearchClient;
import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.bocha.AiBoChaWebSearchClient;
import cn.iocoder.yudao.module.ai.tool.method.PersonService;
import com.alibaba.cloud.ai.dashscope.api.DashScopeApi;
import com.alibaba.cloud.ai.dashscope.api.DashScopeImageApi;
import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel;
import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatOptions;
import com.alibaba.cloud.ai.dashscope.embedding.text.DashScopeEmbeddingModel;
import com.alibaba.cloud.ai.dashscope.embedding.text.DashScopeEmbeddingOptions;
import com.alibaba.cloud.ai.dashscope.image.DashScopeImageModel;
import com.alibaba.cloud.ai.dashscope.image.DashScopeImageOptions;
import io.micrometer.observation.ObservationRegistry;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.deepseek.DeepSeekChatModel;
import org.springframework.ai.deepseek.DeepSeekChatOptions;
import org.springframework.ai.deepseek.api.DeepSeekApi;
import org.springframework.ai.document.MetadataMode;
import org.springframework.ai.embedding.BatchingStrategy;
import org.springframework.ai.embedding.TokenCountBatchingStrategy;
import org.springframework.ai.model.tool.ToolCallingManager;
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.ai.openai.OpenAiChatOptions;
import org.springframework.ai.openai.api.OpenAiApi;
import org.springframework.ai.retry.RetryUtils;
import org.springframework.ai.support.ToolCallbacks;
import org.springframework.ai.tokenizer.JTokkitTokenCountEstimator;
import org.springframework.ai.tokenizer.TokenCountEstimator;
@ -40,11 +53,11 @@ import org.springframework.ai.vectorstore.redis.autoconfigure.RedisVectorStorePr
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.List;
import java.util.Optional;
/**
* 芋道 AI 自动配置
@ -75,31 +88,70 @@ public class AiAutoConfiguration {
// ========== 各种 AI Client 创建 ==========
@Bean
@ConditionalOnProperty(value = "yudao.ai.gemini.enable", havingValue = "true")
public GeminiChatModel geminiChatModel(YudaoAiProperties yudaoAiProperties) {
YudaoAiProperties.Gemini properties = yudaoAiProperties.getGemini();
return buildGeminiChatClient(properties);
@ConditionalOnMissingBean
@ConditionalOnProperty(value = "spring.ai.dashscope.api-key")
public DashScopeChatModel dashScopeChatModel(@Value("${spring.ai.dashscope.api-key}") String apiKey,
ToolCallingManager toolCallingManager,
ObservationRegistry observationRegistry) {
return buildTongYiChatModel(apiKey, toolCallingManager, observationRegistry);
}
public GeminiChatModel buildGeminiChatClient(YudaoAiProperties.Gemini properties) {
if (StrUtil.isEmpty(properties.getModel())) {
properties.setModel(GeminiChatModel.MODEL_DEFAULT);
}
OpenAiChatModel openAiChatModel = OpenAiChatModel.builder()
.openAiApi(OpenAiApi.builder()
.baseUrl(GeminiChatModel.BASE_URL)
.completionsPath(GeminiChatModel.COMPLETE_PATH)
.apiKey(properties.getApiKey())
.build())
.defaultOptions(OpenAiChatOptions.builder()
.model(properties.getModel())
.temperature(properties.getTemperature())
.maxTokens(properties.getMaxTokens())
.topP(properties.getTopP())
.build())
.toolCallingManager(getToolCallingManager())
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(value = "spring.ai.dashscope.api-key")
public DashScopeImageModel dashScopeImageModel(@Value("${spring.ai.dashscope.api-key}") String apiKey,
ObservationRegistry observationRegistry) {
return buildTongYiImagesModel(apiKey, observationRegistry);
}
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(value = "spring.ai.dashscope.api-key")
public DashScopeEmbeddingModel dashScopeEmbeddingModel(@Value("${spring.ai.dashscope.api-key}") String apiKey,
ObservationRegistry observationRegistry) {
return buildTongYiEmbeddingModel(apiKey, null, observationRegistry);
}
public static DashScopeChatModel buildTongYiChatModel(String apiKey) {
return buildTongYiChatModel(apiKey, getToolCallingManager(), getObservationRegistry());
}
private static DashScopeChatModel buildTongYiChatModel(String apiKey, ToolCallingManager toolCallingManager,
ObservationRegistry observationRegistry) {
DashScopeApi dashScopeApi = DashScopeApi.builder().apiKey(apiKey).build();
DashScopeChatOptions options = DashScopeChatOptions.builder()
.model(DashScopeApi.DEFAULT_CHAT_MODEL)
.temperature(0.7)
.build();
return new GeminiChatModel(openAiChatModel);
return new DashScopeChatModel(dashScopeApi, options, toolCallingManager, RetryUtils.DEFAULT_RETRY_TEMPLATE,
observationRegistry);
}
public static DashScopeImageModel buildTongYiImagesModel(String apiKey) {
return buildTongYiImagesModel(apiKey, getObservationRegistry());
}
private static DashScopeImageModel buildTongYiImagesModel(String apiKey, ObservationRegistry observationRegistry) {
DashScopeImageApi dashScopeImageApi = DashScopeImageApi.builder().apiKey(apiKey).build();
DashScopeImageOptions options = DashScopeImageOptions.builder()
.model(DashScopeImageApi.DEFAULT_IMAGE_MODEL)
.build();
return new DashScopeImageModel(dashScopeImageApi, options, RetryUtils.DEFAULT_RETRY_TEMPLATE,
observationRegistry);
}
public static DashScopeEmbeddingModel buildTongYiEmbeddingModel(String apiKey, String model) {
return buildTongYiEmbeddingModel(apiKey, model, getObservationRegistry());
}
private static DashScopeEmbeddingModel buildTongYiEmbeddingModel(String apiKey, String model,
ObservationRegistry observationRegistry) {
DashScopeApi dashScopeApi = DashScopeApi.builder().apiKey(apiKey).build();
DashScopeEmbeddingOptions options = DashScopeEmbeddingOptions.builder()
.model(StrUtil.blankToDefault(model, DashScopeApi.DEFAULT_EMBEDDING_MODEL))
.build();
return new DashScopeEmbeddingModel(dashScopeApi, MetadataMode.EMBED, options, RetryUtils.DEFAULT_RETRY_TEMPLATE,
observationRegistry);
}
@Bean
@ -113,19 +165,18 @@ public class AiAutoConfiguration {
if (StrUtil.isEmpty(properties.getModel())) {
properties.setModel(DouBaoChatModel.MODEL_DEFAULT);
}
OpenAiChatModel openAiChatModel = OpenAiChatModel.builder()
.openAiApi(OpenAiApi.builder()
DeepSeekChatModel openAiChatModel = DeepSeekChatModel.builder()
.deepSeekApi(DeepSeekApi.builder()
.baseUrl(DouBaoChatModel.BASE_URL)
.completionsPath(DouBaoChatModel.COMPLETE_PATH)
.apiKey(properties.getApiKey())
.build())
.defaultOptions(OpenAiChatOptions.builder()
.options(DeepSeekChatOptions.builder()
.model(properties.getModel())
.temperature(properties.getTemperature())
.maxTokens(properties.getMaxTokens())
.topP(properties.getTopP())
.build())
.toolCallingManager(getToolCallingManager())
.build();
return new DouBaoChatModel(openAiChatModel);
}
@ -146,13 +197,12 @@ public class AiAutoConfiguration {
.baseUrl(SiliconFlowApiConstants.DEFAULT_BASE_URL)
.apiKey(properties.getApiKey())
.build())
.defaultOptions(DeepSeekChatOptions.builder()
.options(DeepSeekChatOptions.builder()
.model(properties.getModel())
.temperature(properties.getTemperature())
.maxTokens(properties.getMaxTokens())
.topP(properties.getTopP())
.build())
.toolCallingManager(getToolCallingManager())
.build();
return new SiliconFlowChatModel(openAiChatModel);
}
@ -168,11 +218,8 @@ public class AiAutoConfiguration {
if (StrUtil.isEmpty(properties.getModel())) {
properties.setModel(HunYuanChatModel.MODEL_DEFAULT);
}
// 特殊:由于混元大模型不提供 deepseek而是通过知识引擎所以需要区分下 URL
if (StrUtil.isEmpty(properties.getBaseUrl())) {
properties.setBaseUrl(
StrUtil.startWithIgnoreCase(properties.getModel(), "deepseek") ? HunYuanChatModel.DEEP_SEEK_BASE_URL
: HunYuanChatModel.BASE_URL);
properties.setBaseUrl(HunYuanChatModel.BASE_URL);
}
// 创建 DeepSeekChatModel、HunYuanChatModel 对象
DeepSeekChatModel openAiChatModel = DeepSeekChatModel.builder()
@ -181,13 +228,12 @@ public class AiAutoConfiguration {
.completionsPath(HunYuanChatModel.COMPLETE_PATH)
.apiKey(properties.getApiKey())
.build())
.defaultOptions(DeepSeekChatOptions.builder()
.options(DeepSeekChatOptions.builder()
.model(properties.getModel())
.temperature(properties.getTemperature())
.maxTokens(properties.getMaxTokens())
.topP(properties.getTopP())
.build())
.toolCallingManager(getToolCallingManager())
.build();
return new HunYuanChatModel(openAiChatModel);
}
@ -203,25 +249,15 @@ public class AiAutoConfiguration {
if (StrUtil.isEmpty(properties.getModel())) {
properties.setModel(XingHuoChatModel.MODEL_DEFAULT);
}
OpenAiApi.Builder builder = OpenAiApi.builder()
.baseUrl(XingHuoChatModel.BASE_URL_V1)
.apiKey(properties.getAppKey() + ":" + properties.getSecretKey());
if ("x1".equals(properties.getModel())) {
builder.baseUrl(XingHuoChatModel.BASE_URL_V2)
.completionsPath(XingHuoChatModel.BASE_COMPLETIONS_PATH_V2);
}
OpenAiChatModel openAiChatModel = OpenAiChatModel.builder()
.openAiApi(builder.build())
.defaultOptions(OpenAiChatOptions.builder()
return XingHuoChatModel.builder()
.apiKey(properties.getApiKey())
.options(DeepSeekChatOptions.builder()
.model(properties.getModel())
.temperature(properties.getTemperature())
.maxTokens(properties.getMaxTokens())
.topP(properties.getTopP())
.build())
// TODO @芋艿:星火的 function call 有 bug会报 ToolResponseMessage must have an id 错误!!!
.toolCallingManager(getToolCallingManager())
.build();
return new XingHuoChatModel(openAiChatModel);
}
@Bean
@ -235,20 +271,135 @@ public class AiAutoConfiguration {
if (StrUtil.isEmpty(properties.getModel())) {
properties.setModel(BaiChuanChatModel.MODEL_DEFAULT);
}
OpenAiChatModel openAiChatModel = OpenAiChatModel.builder()
.openAiApi(OpenAiApi.builder()
DeepSeekChatModel deepSeekChatModel = DeepSeekChatModel.builder()
.deepSeekApi(DeepSeekApi.builder()
.baseUrl(BaiChuanChatModel.BASE_URL)
.apiKey(properties.getApiKey())
.completionsPath(BaiChuanChatModel.COMPLETE_PATH)
.build())
.defaultOptions(OpenAiChatOptions.builder()
.options(DeepSeekChatOptions.builder()
.model(properties.getModel())
.temperature(properties.getTemperature())
.maxTokens(properties.getMaxTokens())
.topP(properties.getTopP())
.build())
.toolCallingManager(getToolCallingManager())
.build();
return new BaiChuanChatModel(openAiChatModel);
return new BaiChuanChatModel(deepSeekChatModel);
}
@Bean
@ConditionalOnProperty(value = "yudao.ai.yiyan.enable", havingValue = "true")
public YiYanChatModel yiYanChatClient(YudaoAiProperties yudaoAiProperties) {
YudaoAiProperties.YiYan properties = yudaoAiProperties.getYiyan();
return buildYiYanChatClient(properties);
}
public YiYanChatModel buildYiYanChatClient(YudaoAiProperties.YiYan properties) {
if (StrUtil.isEmpty(properties.getModel())) {
properties.setModel(YiYanChatModel.MODEL_DEFAULT);
}
return new YiYanChatModel(buildDeepSeekCompatibleChatModel(
StrUtil.blankToDefault(properties.getBaseUrl(), YiYanChatModel.BASE_URL),
null, properties.getApiKey(), properties.getModel(), properties.getTemperature(),
properties.getMaxTokens(), properties.getTopP()));
}
@Bean
@ConditionalOnProperty(value = "yudao.ai.zhipu.enable", havingValue = "true")
public ZhiPuChatModel zhiPuChatClient(YudaoAiProperties yudaoAiProperties) {
YudaoAiProperties.ZhiPu properties = yudaoAiProperties.getZhipu();
return buildZhiPuChatClient(properties);
}
public ZhiPuChatModel buildZhiPuChatClient(YudaoAiProperties.ZhiPu properties) {
if (StrUtil.isEmpty(properties.getModel())) {
properties.setModel(ZhiPuChatModel.MODEL_DEFAULT);
}
DeepSeekChatModel deepSeekChatModel = DeepSeekChatModel.builder()
.deepSeekApi(DeepSeekApi.builder()
.baseUrl(StrUtil.blankToDefault(properties.getBaseUrl(), ZhiPuChatModel.BASE_URL))
.apiKey(properties.getApiKey())
.build())
.options(DeepSeekChatOptions.builder()
.model(properties.getModel())
.temperature(properties.getTemperature())
.maxTokens(properties.getMaxTokens())
.topP(properties.getTopP())
.build())
.build();
return new ZhiPuChatModel(deepSeekChatModel);
}
@Bean
@ConditionalOnProperty(value = "yudao.ai.minimax.enable", havingValue = "true")
public MiniMaxChatModel miniMaxChatClient(YudaoAiProperties yudaoAiProperties) {
YudaoAiProperties.MiniMax properties = yudaoAiProperties.getMinimax();
return buildMiniMaxChatClient(properties);
}
public MiniMaxChatModel buildMiniMaxChatClient(YudaoAiProperties.MiniMax properties) {
if (StrUtil.isEmpty(properties.getModel())) {
properties.setModel(MiniMaxChatModel.MODEL_DEFAULT);
}
return new MiniMaxChatModel(buildDeepSeekCompatibleChatModel(
StrUtil.blankToDefault(properties.getBaseUrl(), MiniMaxChatModel.BASE_URL),
null, properties.getApiKey(), properties.getModel(), properties.getTemperature(),
properties.getMaxTokens(), properties.getTopP()));
}
@Bean
@ConditionalOnProperty(value = "yudao.ai.moonshot.enable", havingValue = "true")
public MoonshotChatModel moonshotChatClient(YudaoAiProperties yudaoAiProperties) {
YudaoAiProperties.Moonshot properties = yudaoAiProperties.getMoonshot();
return buildMoonshotChatClient(properties);
}
public MoonshotChatModel buildMoonshotChatClient(YudaoAiProperties.Moonshot properties) {
if (StrUtil.isEmpty(properties.getModel())) {
properties.setModel(MoonshotChatModel.MODEL_DEFAULT);
}
return new MoonshotChatModel(buildDeepSeekCompatibleChatModel(
StrUtil.blankToDefault(properties.getBaseUrl(), MoonshotChatModel.BASE_URL),
MoonshotChatModel.COMPLETE_PATH, properties.getApiKey(), properties.getModel(),
properties.getTemperature(), properties.getMaxTokens(), properties.getTopP()));
}
@Bean
@ConditionalOnProperty(value = "yudao.ai.stepfun.enable", havingValue = "true")
public StepFunChatModel stepFunChatClient(YudaoAiProperties yudaoAiProperties) {
YudaoAiProperties.StepFun properties = yudaoAiProperties.getStepfun();
return buildStepFunChatClient(properties);
}
public StepFunChatModel buildStepFunChatClient(YudaoAiProperties.StepFun properties) {
if (StrUtil.isEmpty(properties.getModel())) {
properties.setModel(StepFunChatModel.MODEL_DEFAULT);
}
return new StepFunChatModel(buildDeepSeekCompatibleChatModel(
StrUtil.blankToDefault(properties.getBaseUrl(), StepFunChatModel.BASE_URL),
StepFunChatModel.COMPLETE_PATH, properties.getApiKey(), properties.getModel(),
properties.getTemperature(), properties.getMaxTokens(), properties.getTopP()));
}
private static DeepSeekChatModel buildDeepSeekCompatibleChatModel(String baseUrl, String completionsPath,
String apiKey, String model,
Double temperature, Integer maxTokens,
Double topP) {
DeepSeekApi.Builder apiBuilder = DeepSeekApi.builder()
.baseUrl(baseUrl)
.apiKey(apiKey);
if (StrUtil.isNotEmpty(completionsPath)) {
apiBuilder.completionsPath(completionsPath);
}
return DeepSeekChatModel.builder()
.deepSeekApi(apiBuilder.build())
.options(DeepSeekChatOptions.builder()
.model(model)
.temperature(temperature)
.maxTokens(maxTokens)
.topP(topP)
.build())
.build();
}
@Bean
@ -264,26 +415,28 @@ public class AiAutoConfiguration {
return new SunoApi(yudaoAiProperties.getSuno().getBaseUrl());
}
public ChatModel buildGrokChatClient(YudaoAiProperties.Grok properties) {
@Bean
@ConditionalOnProperty(value = "yudao.ai.grok.enable", havingValue = "true")
public GrokChatModel grokChatClient(YudaoAiProperties yudaoAiProperties) {
YudaoAiProperties.Grok properties = yudaoAiProperties.getGrok();
return buildGrokChatClient(properties);
}
public GrokChatModel buildGrokChatClient(YudaoAiProperties.Grok properties) {
if (StrUtil.isEmpty(properties.getModel())) {
properties.setModel(GrokChatModel.MODEL_DEFAULT);
}
OpenAiChatModel openAiChatModel = OpenAiChatModel.builder()
.openAiApi(OpenAiApi.builder()
.baseUrl(Optional.ofNullable(properties.getBaseUrl())
.orElse(GrokChatModel.BASE_URL))
.completionsPath(GrokChatModel.COMPLETE_PATH)
.options(OpenAiChatOptions.builder()
.baseUrl(StrUtil.blankToDefault(properties.getBaseUrl(), GrokChatModel.BASE_URL))
.apiKey(properties.getApiKey())
.build())
.defaultOptions(OpenAiChatOptions.builder()
.model(properties.getModel())
.temperature(properties.getTemperature())
.maxTokens(properties.getMaxTokens())
.topP(properties.getTopP())
.build())
.toolCallingManager(getToolCallingManager())
.build();
return new DouBaoChatModel(openAiChatModel);
return new GrokChatModel(openAiChatModel);
}
// ========== RAG 相关 ==========
@ -302,6 +455,10 @@ public class AiAutoConfiguration {
return SpringUtil.getBean(ToolCallingManager.class);
}
private static ObservationRegistry getObservationRegistry() {
return SpringUtil.getBean(ObservationRegistry.class);
}
// ========== Web Search 相关 ==========
@Bean
@ -320,4 +477,4 @@ public class AiAutoConfiguration {
return List.of(ToolCallbacks.from(personService));
}
}
}

View File

@ -13,11 +13,6 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
@Data
public class YudaoAiProperties {
/**
* 谷歌 Gemini
*/
private Gemini gemini;
/**
* 字节豆包
*/
@ -43,6 +38,36 @@ public class YudaoAiProperties {
*/
private BaiChuan baichuan;
/**
* 文心一言
*/
private YiYan yiyan;
/**
* 智谱
*/
private ZhiPu zhipu;
/**
* MiniMax
*/
private MiniMax minimax;
/**
* 月之暗面
*/
private Moonshot moonshot;
/**
* 阶跃星辰
*/
private StepFun stepfun;
/**
* Grok
*/
private Grok grok;
/**
* Midjourney 绘图
*/
@ -59,19 +84,6 @@ public class YudaoAiProperties {
*/
private WebSearch webSearch;
@Data
public static class Gemini {
private String enable;
private String apiKey;
private String model;
private Double temperature;
private Integer maxTokens;
private Double topP;
}
@Data
public static class DouBao {
@ -116,9 +128,7 @@ public class YudaoAiProperties {
public static class XingHuo {
private String enable;
private String appId;
private String appKey;
private String secretKey;
private String apiKey;
private String model;
private Double temperature;
@ -140,6 +150,62 @@ public class YudaoAiProperties {
}
@Data
public static class YiYan {
private String enable;
private String baseUrl;
private String apiKey;
private String model;
private Double temperature;
private Integer maxTokens;
private Double topP;
}
@Data
public static class ZhiPu {
private String enable;
private String baseUrl;
private String apiKey;
private String model;
private Double temperature;
private Integer maxTokens;
private Double topP;
}
@Data
public static class MiniMax {
private String enable;
private String baseUrl;
private String apiKey;
private String model;
private Double temperature;
private Integer maxTokens;
private Double topP;
}
@Data
public static class Moonshot {
private String enable;
private String baseUrl;
private String apiKey;
private String model;
private Double temperature;
private Integer maxTokens;
private Double topP;
}
@Data
public static class Midjourney {
@ -174,6 +240,20 @@ public class YudaoAiProperties {
}
@Data
public static class StepFun {
private String enable;
private String apiKey;
private String baseUrl;
private String model;
private Double temperature;
private Integer maxTokens;
private Double topP;
}
@Data
public static class WebSearch {

View File

@ -8,52 +8,40 @@ import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.RuntimeUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.framework.common.util.spring.SpringUtils;
import cn.iocoder.yudao.module.ai.enums.model.AiPlatformEnum;
import cn.iocoder.yudao.module.ai.framework.ai.config.AiAutoConfiguration;
import cn.iocoder.yudao.module.ai.framework.ai.config.YudaoAiProperties;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.baichuan.BaiChuanChatModel;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.doubao.DouBaoChatModel;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.gemini.GeminiChatModel;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.hunyuan.HunYuanChatModel;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.midjourney.api.MidjourneyApi;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.minimax.MiniMaxChatModel;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.moonshot.MoonshotChatModel;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.siliconflow.SiliconFlowApiConstants;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.siliconflow.SiliconFlowChatModel;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.siliconflow.SiliconFlowImageApi;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.siliconflow.SiliconFlowImageModel;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.stepfun.StepFunChatModel;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.suno.api.SunoApi;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.xinghuo.XingHuoChatModel;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.yiyan.YiYanChatModel;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.zhipu.ZhiPuChatModel;
import cn.iocoder.yudao.module.ai.util.AiUtils;
import com.alibaba.cloud.ai.autoconfigure.dashscope.DashScopeChatAutoConfiguration;
import com.alibaba.cloud.ai.autoconfigure.dashscope.DashScopeEmbeddingAutoConfiguration;
import com.alibaba.cloud.ai.autoconfigure.dashscope.DashScopeImageAutoConfiguration;
import com.alibaba.cloud.ai.dashscope.api.DashScopeApi;
import com.alibaba.cloud.ai.dashscope.api.DashScopeImageApi;
import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel;
import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatOptions;
import com.alibaba.cloud.ai.dashscope.embedding.DashScopeEmbeddingModel;
import com.alibaba.cloud.ai.dashscope.embedding.DashScopeEmbeddingOptions;
import com.alibaba.cloud.ai.dashscope.embedding.text.DashScopeEmbeddingModel;
import com.alibaba.cloud.ai.dashscope.image.DashScopeImageModel;
import com.azure.ai.openai.OpenAIClientBuilder;
import com.azure.core.credential.KeyCredential;
import com.google.genai.Client;
import com.google.genai.types.HttpOptions;
import io.micrometer.observation.ObservationRegistry;
import io.milvus.client.MilvusServiceClient;
import io.qdrant.client.QdrantClient;
import io.qdrant.client.QdrantGrpcClient;
import lombok.SneakyThrows;
import org.springaicommunity.moonshot.MoonshotChatModel;
import org.springaicommunity.moonshot.MoonshotChatOptions;
import org.springaicommunity.moonshot.api.MoonshotApi;
import org.springaicommunity.moonshot.autoconfigure.MoonshotChatAutoConfiguration;
import org.springaicommunity.qianfan.QianFanChatModel;
import org.springaicommunity.qianfan.QianFanEmbeddingModel;
import org.springaicommunity.qianfan.QianFanEmbeddingOptions;
import org.springaicommunity.qianfan.QianFanImageModel;
import org.springaicommunity.qianfan.api.QianFanApi;
import org.springaicommunity.qianfan.api.QianFanImageApi;
import org.springaicommunity.qianfan.autoconfigure.QianFanChatAutoConfiguration;
import org.springaicommunity.qianfan.autoconfigure.QianFanEmbeddingAutoConfiguration;
import org.springframework.ai.azure.openai.AzureOpenAiChatModel;
import org.springframework.ai.azure.openai.AzureOpenAiEmbeddingModel;
import org.springframework.ai.anthropic.AnthropicChatModel;
import org.springframework.ai.anthropic.AnthropicChatOptions;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.deepseek.DeepSeekChatModel;
import org.springframework.ai.deepseek.DeepSeekChatOptions;
@ -62,41 +50,24 @@ import org.springframework.ai.document.MetadataMode;
import org.springframework.ai.embedding.BatchingStrategy;
import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.ai.embedding.observation.EmbeddingModelObservationConvention;
import org.springframework.ai.google.genai.GoogleGenAiChatModel;
import org.springframework.ai.google.genai.GoogleGenAiChatOptions;
import org.springframework.ai.image.ImageModel;
import org.springframework.ai.minimax.MiniMaxChatModel;
import org.springframework.ai.minimax.MiniMaxChatOptions;
import org.springframework.ai.minimax.MiniMaxEmbeddingModel;
import org.springframework.ai.minimax.MiniMaxEmbeddingOptions;
import org.springframework.ai.minimax.api.MiniMaxApi;
import org.springframework.ai.model.anthropic.autoconfigure.AnthropicChatAutoConfiguration;
import org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiChatAutoConfiguration;
import org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiEmbeddingAutoConfiguration;
import org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiEmbeddingProperties;
import org.springframework.ai.model.deepseek.autoconfigure.DeepSeekChatAutoConfiguration;
import org.springframework.ai.model.minimax.autoconfigure.MiniMaxChatAutoConfiguration;
import org.springframework.ai.model.minimax.autoconfigure.MiniMaxEmbeddingAutoConfiguration;
import org.springframework.ai.model.google.genai.autoconfigure.chat.GoogleGenAiChatAutoConfiguration;
import org.springframework.ai.model.ollama.autoconfigure.OllamaChatAutoConfiguration;
import org.springframework.ai.model.openai.autoconfigure.OpenAiChatAutoConfiguration;
import org.springframework.ai.model.openai.autoconfigure.OpenAiEmbeddingAutoConfiguration;
import org.springframework.ai.model.openai.autoconfigure.OpenAiImageAutoConfiguration;
import org.springframework.ai.model.stabilityai.autoconfigure.StabilityAiImageAutoConfiguration;
import org.springframework.ai.model.tool.ToolCallingManager;
import org.springframework.ai.model.zhipuai.autoconfigure.ZhiPuAiChatAutoConfiguration;
import org.springframework.ai.model.zhipuai.autoconfigure.ZhiPuAiEmbeddingAutoConfiguration;
import org.springframework.ai.model.zhipuai.autoconfigure.ZhiPuAiImageAutoConfiguration;
import org.springframework.ai.ollama.OllamaChatModel;
import org.springframework.ai.ollama.OllamaEmbeddingModel;
import org.springframework.ai.ollama.api.OllamaApi;
import org.springframework.ai.ollama.api.OllamaEmbeddingOptions;
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.ai.openai.OpenAiEmbeddingModel;
import org.springframework.ai.openai.OpenAiEmbeddingOptions;
import org.springframework.ai.openai.OpenAiImageModel;
import org.springframework.ai.openai.api.OpenAiApi;
import org.springframework.ai.openai.api.OpenAiImageApi;
import org.springframework.ai.openai.api.common.OpenAiApiConstants;
import org.springframework.ai.anthropic.AnthropicChatModel;
import org.springframework.ai.anthropic.api.AnthropicApi;
import org.springframework.ai.openai.*;
import org.springframework.ai.retry.RetryUtils;
import org.springframework.ai.stabilityai.StabilityAiImageModel;
import org.springframework.ai.stabilityai.api.StabilityAiApi;
import org.springframework.ai.vectorstore.SimpleVectorStore;
@ -114,24 +85,22 @@ import org.springframework.ai.vectorstore.qdrant.autoconfigure.QdrantVectorStore
import org.springframework.ai.vectorstore.redis.RedisVectorStore;
import org.springframework.ai.vectorstore.redis.autoconfigure.RedisVectorStoreAutoConfiguration;
import org.springframework.ai.vectorstore.redis.autoconfigure.RedisVectorStoreProperties;
import org.springframework.ai.zhipuai.*;
import org.springframework.ai.zhipuai.api.ZhiPuAiApi;
import org.springframework.ai.zhipuai.api.ZhiPuAiImageApi;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.web.client.RestClient;
import redis.clients.jedis.JedisPooled;
import org.springframework.boot.data.redis.autoconfigure.DataRedisProperties;
import redis.clients.jedis.DefaultJedisClientConfig;
import redis.clients.jedis.HostAndPort;
import redis.clients.jedis.JedisClientConfig;
import redis.clients.jedis.RedisClient;
import java.io.File;
import java.time.Duration;
import java.util.List;
import java.util.Collections;
import java.util.Map;
import java.util.Timer;
import java.util.TimerTask;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
import static org.springframework.ai.retry.RetryUtils.DEFAULT_RETRY_TEMPLATE;
/**
* AI Model 模型工厂的实现类
@ -141,7 +110,9 @@ import static org.springframework.ai.retry.RetryUtils.DEFAULT_RETRY_TEMPLATE;
public class AiModelFactoryImpl implements AiModelFactory {
@Override
public ChatModel getOrCreateChatModel(AiPlatformEnum platform, String apiKey, String url) {
public ChatModel getOrCreateChatModel(AiPlatformEnum platform, String rawApiKey, String rawUrl) {
final String apiKey = resolveSpringPlaceholders(rawApiKey);
final String url = resolveSpringPlaceholders(rawUrl);
String cacheKey = buildClientCacheKey(ChatModel.class, platform, apiKey, url);
return Singleton.get(cacheKey, (Func0<ChatModel>) () -> {
// noinspection EnhancedSwitchMigration
@ -164,6 +135,8 @@ public class AiModelFactoryImpl implements AiModelFactory {
return buildMiniMaxChatModel(apiKey, url);
case MOONSHOT:
return buildMoonshotChatModel(apiKey, url);
case STEP_FUN:
return buildStepFunChatModel(apiKey, url);
case XING_HUO:
return buildXingHuoChatModel(apiKey);
case BAI_CHUAN:
@ -175,11 +148,11 @@ public class AiModelFactoryImpl implements AiModelFactory {
case ANTHROPIC:
return buildAnthropicChatModel(apiKey, url);
case GEMINI:
return buildGeminiChatModel(apiKey);
return buildGeminiChatModel(apiKey, url);
case OLLAMA:
return buildOllamaChatModel(url);
case GROK:
return buildGrokChatModel(apiKey,url);
return buildGrokChatModel(apiKey, url);
default:
throw new IllegalArgumentException(StrUtil.format("未知平台({})", platform));
}
@ -193,7 +166,7 @@ public class AiModelFactoryImpl implements AiModelFactory {
case TONG_YI:
return SpringUtil.getBean(DashScopeChatModel.class);
case YI_YAN:
return SpringUtil.getBean(QianFanChatModel.class);
return SpringUtil.getBean(YiYanChatModel.class);
case DEEP_SEEK:
return SpringUtil.getBean(DeepSeekChatModel.class);
case DOU_BAO:
@ -203,23 +176,23 @@ public class AiModelFactoryImpl implements AiModelFactory {
case SILICON_FLOW:
return SpringUtil.getBean(SiliconFlowChatModel.class);
case ZHI_PU:
return SpringUtil.getBean(ZhiPuAiChatModel.class);
return SpringUtil.getBean(ZhiPuChatModel.class);
case MINI_MAX:
return SpringUtil.getBean(MiniMaxChatModel.class);
case MOONSHOT:
return SpringUtil.getBean(MoonshotChatModel.class);
case STEP_FUN:
return SpringUtil.getBean(StepFunChatModel.class);
case XING_HUO:
return SpringUtil.getBean(XingHuoChatModel.class);
case BAI_CHUAN:
return SpringUtil.getBean(BaiChuanChatModel.class);
case OPENAI:
return SpringUtil.getBean(OpenAiChatModel.class);
case AZURE_OPENAI:
return SpringUtil.getBean(AzureOpenAiChatModel.class);
case ANTHROPIC:
return SpringUtil.getBean(AnthropicChatModel.class);
case GEMINI:
return SpringUtil.getBean(GeminiChatModel.class);
return SpringUtil.getBean(GoogleGenAiChatModel.class);
case OLLAMA:
return SpringUtil.getBean(OllamaChatModel.class);
default:
@ -233,10 +206,6 @@ public class AiModelFactoryImpl implements AiModelFactory {
switch (platform) {
case TONG_YI:
return SpringUtil.getBean(DashScopeImageModel.class);
case YI_YAN:
return SpringUtil.getBean(QianFanImageModel.class);
case ZHI_PU:
return SpringUtil.getBean(ZhiPuAiImageModel.class);
case SILICON_FLOW:
return SpringUtil.getBean(SiliconFlowImageModel.class);
case OPENAI:
@ -249,19 +218,17 @@ public class AiModelFactoryImpl implements AiModelFactory {
}
@Override
public ImageModel getOrCreateImageModel(AiPlatformEnum platform, String apiKey, String url) {
public ImageModel getOrCreateImageModel(AiPlatformEnum platform, String rawApiKey, String rawUrl) {
String apiKey = resolveSpringPlaceholders(rawApiKey);
String url = resolveSpringPlaceholders(rawUrl);
// noinspection EnhancedSwitchMigration
switch (platform) {
case TONG_YI:
return buildTongYiImagesModel(apiKey);
case YI_YAN:
return buildQianFanImageModel(apiKey);
case ZHI_PU:
return buildZhiPuAiImageModel(apiKey, url);
case OPENAI:
return buildOpenAiImageModel(apiKey, url);
case SILICON_FLOW:
return buildSiliconFlowImageModel(apiKey,url);
return buildSiliconFlowImageModel(apiKey, url);
case STABLE_DIFFUSION:
return buildStabilityAiImageModel(apiKey, url);
default:
@ -270,9 +237,11 @@ public class AiModelFactoryImpl implements AiModelFactory {
}
@Override
public MidjourneyApi getOrCreateMidjourneyApi(String apiKey, String url) {
String cacheKey = buildClientCacheKey(MidjourneyApi.class, AiPlatformEnum.MIDJOURNEY.getPlatform(), apiKey,
url);
public MidjourneyApi getOrCreateMidjourneyApi(String rawApiKey, String rawUrl) {
final String apiKey = resolveSpringPlaceholders(rawApiKey);
final String url = resolveSpringPlaceholders(rawUrl);
String cacheKey = buildClientCacheKey(MidjourneyApi.class, AiPlatformEnum.MIDJOURNEY.getPlatform(),
apiKey, url);
return Singleton.get(cacheKey, (Func0<MidjourneyApi>) () -> {
YudaoAiProperties.Midjourney properties = SpringUtil.getBean(YudaoAiProperties.class)
.getMidjourney();
@ -281,25 +250,23 @@ public class AiModelFactoryImpl implements AiModelFactory {
}
@Override
public SunoApi getOrCreateSunoApi(String apiKey, String url) {
public SunoApi getOrCreateSunoApi(String rawApiKey, String rawUrl) {
final String apiKey = resolveSpringPlaceholders(rawApiKey);
final String url = resolveSpringPlaceholders(rawUrl);
String cacheKey = buildClientCacheKey(SunoApi.class, AiPlatformEnum.SUNO.getPlatform(), apiKey, url);
return Singleton.get(cacheKey, (Func0<SunoApi>) () -> new SunoApi(url));
}
@Override
@SuppressWarnings("EnhancedSwitchMigration")
public EmbeddingModel getOrCreateEmbeddingModel(AiPlatformEnum platform, String apiKey, String url, String model) {
public EmbeddingModel getOrCreateEmbeddingModel(AiPlatformEnum platform, String rawApiKey, String rawUrl, String model) {
final String apiKey = resolveSpringPlaceholders(rawApiKey);
final String url = resolveSpringPlaceholders(rawUrl);
String cacheKey = buildClientCacheKey(EmbeddingModel.class, platform, apiKey, url, model);
return Singleton.get(cacheKey, (Func0<EmbeddingModel>) () -> {
switch (platform) {
case TONG_YI:
return buildTongYiEmbeddingModel(apiKey, model);
case YI_YAN:
return buildYiYanEmbeddingModel(apiKey, model);
case ZHI_PU:
return buildZhiPuEmbeddingModel(apiKey, url, model);
case MINI_MAX:
return buildMiniMaxEmbeddingModel(apiKey, url, model);
case OPENAI:
return buildOpenAiEmbeddingModel(apiKey, url, model);
case AZURE_OPENAI:
@ -341,56 +308,31 @@ public class AiModelFactoryImpl implements AiModelFactory {
return StrUtil.format("{}#{}", clazz.getName(), ArrayUtil.join(params, "_"));
}
private static String resolveSpringPlaceholders(String value) {
// yml 配置的占位符由 Spring 自动解析DB 里保存的 ${xxx} 需要在这里手动解析。
return AiUtils.resolveSpringPlaceholders(value);
}
// ========== 各种创建 spring-ai 客户端的方法 ==========
/**
* 可参考 {@link DashScopeChatAutoConfiguration} 的 dashscopeChatModel 方法
*/
private static DashScopeChatModel buildTongYiChatModel(String key) {
DashScopeApi dashScopeApi = DashScopeApi.builder().apiKey(key).build();
DashScopeChatOptions options = DashScopeChatOptions.builder().withModel(DashScopeApi.DEFAULT_CHAT_MODEL)
.withTemperature(0.7).build();
return DashScopeChatModel.builder()
.dashScopeApi(dashScopeApi)
.defaultOptions(options)
.toolCallingManager(getToolCallingManager())
.build();
return AiAutoConfiguration.buildTongYiChatModel(key);
}
/**
* 可参考 {@link DashScopeImageAutoConfiguration} 的 dashScopeImageModel 方法
*/
private static DashScopeImageModel buildTongYiImagesModel(String key) {
DashScopeImageApi dashScopeImageApi = DashScopeImageApi.builder().apiKey(key).build();
return DashScopeImageModel.builder()
.dashScopeApi(dashScopeImageApi)
.build();
return AiAutoConfiguration.buildTongYiImagesModel(key);
}
/**
* 可参考 {@link QianFanChatAutoConfiguration} 的 qianFanChatModel 方法
*/
private static QianFanChatModel buildYiYanChatModel(String key) {
// TODO spring ai qianfan 有 bug无法使用 https://github.com/spring-ai-community/qianfan/issues/6
List<String> keys = StrUtil.split(key, '|');
Assert.equals(keys.size(), 2, "YiYanChatClient 的密钥需要 (appKey|secretKey) 格式");
String appKey = keys.get(0);
String secretKey = keys.get(1);
QianFanApi qianFanApi = new QianFanApi(appKey, secretKey);
return new QianFanChatModel(qianFanApi);
}
/**
* 可参考 {@link QianFanEmbeddingAutoConfiguration} 的 qianFanImageModel 方法
*/
private QianFanImageModel buildQianFanImageModel(String key) {
// TODO spring ai qianfan 有 bug无法使用 https://github.com/spring-ai-community/qianfan/issues/6
List<String> keys = StrUtil.split(key, '|');
Assert.equals(keys.size(), 2, "YiYanChatClient 的密钥需要 (appKey|secretKey) 格式");
String appKey = keys.get(0);
String secretKey = keys.get(1);
QianFanImageApi qianFanApi = new QianFanImageApi(appKey, secretKey);
return new QianFanImageModel(qianFanApi);
private ChatModel buildYiYanChatModel(String apiKey) {
YudaoAiProperties.YiYan properties = new YudaoAiProperties.YiYan()
.setApiKey(apiKey);
return new AiAutoConfiguration().buildYiYanChatClient(properties);
}
/**
@ -402,8 +344,7 @@ public class AiModelFactoryImpl implements AiModelFactory {
.temperature(0.7).build();
return DeepSeekChatModel.builder()
.deepSeekApi(deepSeekApi)
.defaultOptions(options)
.toolCallingManager(getToolCallingManager())
.options(options)
.build();
}
@ -435,62 +376,47 @@ public class AiModelFactoryImpl implements AiModelFactory {
}
/**
* 可参考 {@link ZhiPuAiChatAutoConfiguration} 的 zhiPuAiChatModel 方法
* 可参考 {@link AiAutoConfiguration#zhiPuChatClient(YudaoAiProperties)}
*/
private ZhiPuAiChatModel buildZhiPuChatModel(String apiKey, String url) {
ZhiPuAiApi.Builder zhiPuAiApiBuilder = ZhiPuAiApi.builder().apiKey(apiKey);
if (StrUtil.isNotEmpty(url)) {
zhiPuAiApiBuilder.baseUrl(url);
}
ZhiPuAiChatOptions options = ZhiPuAiChatOptions.builder().model(ZhiPuAiApi.DEFAULT_CHAT_MODEL).temperature(0.7).build();
return new ZhiPuAiChatModel(zhiPuAiApiBuilder.build(), options, getToolCallingManager(), DEFAULT_RETRY_TEMPLATE,
getObservationRegistry().getIfAvailable());
private ZhiPuChatModel buildZhiPuChatModel(String apiKey, String url) {
YudaoAiProperties.ZhiPu properties = new YudaoAiProperties.ZhiPu()
.setBaseUrl(url).setApiKey(apiKey);
return new AiAutoConfiguration().buildZhiPuChatClient(properties);
}
/**
* 可参考 {@link ZhiPuAiImageAutoConfiguration} 的 zhiPuAiImageModel 方法
*/
private ZhiPuAiImageModel buildZhiPuAiImageModel(String apiKey, String url) {
ZhiPuAiImageApi zhiPuAiApi = StrUtil.isEmpty(url) ? new ZhiPuAiImageApi(apiKey)
: new ZhiPuAiImageApi(url, apiKey, RestClient.builder());
return new ZhiPuAiImageModel(zhiPuAiApi);
}
/**
* 可参考 {@link MiniMaxChatAutoConfiguration} 的 miniMaxChatModel 方法
* 可参考 {@link AiAutoConfiguration#miniMaxChatClient(YudaoAiProperties)}
*/
private MiniMaxChatModel buildMiniMaxChatModel(String apiKey, String url) {
MiniMaxApi miniMaxApi = StrUtil.isEmpty(url) ? new MiniMaxApi(apiKey)
: new MiniMaxApi(url, apiKey);
MiniMaxChatOptions options = MiniMaxChatOptions.builder().model(MiniMaxApi.DEFAULT_CHAT_MODEL).temperature(0.7).build();
return new MiniMaxChatModel(miniMaxApi, options, getToolCallingManager(), DEFAULT_RETRY_TEMPLATE);
YudaoAiProperties.MiniMax properties = new YudaoAiProperties.MiniMax()
.setBaseUrl(url).setApiKey(apiKey);
return new AiAutoConfiguration().buildMiniMaxChatClient(properties);
}
/**
* 可参考 {@link MoonshotChatAutoConfiguration} 的 moonshotChatModel 方法
* 可参考 {@link AiAutoConfiguration#moonshotChatClient(YudaoAiProperties)}
*/
private MoonshotChatModel buildMoonshotChatModel(String apiKey, String url) {
MoonshotApi.Builder moonshotApiBuilder = MoonshotApi.builder()
.apiKey(apiKey);
if (StrUtil.isNotEmpty(url)) {
moonshotApiBuilder.baseUrl(url);
}
MoonshotChatOptions options = MoonshotChatOptions.builder().model(MoonshotApi.DEFAULT_CHAT_MODEL).build();
return MoonshotChatModel.builder()
.moonshotApi(moonshotApiBuilder.build())
.defaultOptions(options)
.toolCallingManager(getToolCallingManager())
.build();
YudaoAiProperties.Moonshot properties = new YudaoAiProperties.Moonshot()
.setBaseUrl(url).setApiKey(apiKey);
return new AiAutoConfiguration().buildMoonshotChatClient(properties);
}
/**
* 可参考 {@link AiAutoConfiguration#stepFunChatClient(YudaoAiProperties)}
*/
private StepFunChatModel buildStepFunChatModel(String apiKey, String url) {
YudaoAiProperties.StepFun properties = new YudaoAiProperties.StepFun()
.setBaseUrl(url).setApiKey(apiKey);
return new AiAutoConfiguration().buildStepFunChatClient(properties);
}
/**
* 可参考 {@link AiAutoConfiguration#xingHuoChatClient(YudaoAiProperties)}
*/
private static XingHuoChatModel buildXingHuoChatModel(String key) {
List<String> keys = StrUtil.split(key, '|');
Assert.equals(keys.size(), 2, "XingHuoChatClient 的密钥需要 (appKey|secretKey) 格式");
private static XingHuoChatModel buildXingHuoChatModel(String apiKey) {
YudaoAiProperties.XingHuo properties = new YudaoAiProperties.XingHuo()
.setAppKey(keys.get(0)).setSecretKey(keys.get(1));
.setApiKey(apiKey).setModel(XingHuoChatModel.MODEL_DEFAULT);
return new AiAutoConfiguration().buildXingHuoChatClient(properties);
}
@ -507,58 +433,74 @@ public class AiModelFactoryImpl implements AiModelFactory {
* 可参考 {@link OpenAiChatAutoConfiguration} 的 openAiChatModel 方法
*/
private static OpenAiChatModel buildOpenAiChatModel(String openAiToken, String url) {
url = StrUtil.blankToDefault(url, OpenAiApiConstants.DEFAULT_BASE_URL);
OpenAiApi openAiApi = OpenAiApi.builder().baseUrl(url).apiKey(openAiToken).build();
return OpenAiChatModel.builder()
.openAiApi(openAiApi)
.toolCallingManager(getToolCallingManager())
.options(buildOpenAiChatOptions(openAiToken, url).build())
.build();
}
/**
* 可参考 {@link AzureOpenAiChatAutoConfiguration}
*/
private static AzureOpenAiChatModel buildAzureOpenAiChatModel(String apiKey, String url) {
// TODO @芋艿:使用前,请测试,暂时没密钥!!!
OpenAIClientBuilder openAIClientBuilder = new OpenAIClientBuilder()
.endpoint(url).credential(new KeyCredential(apiKey));
return AzureOpenAiChatModel.builder()
.openAIClientBuilder(openAIClientBuilder)
.toolCallingManager(getToolCallingManager())
private static OpenAiChatModel buildAzureOpenAiChatModel(String openAiToken, String url) {
return OpenAiChatModel.builder()
.options(buildOpenAiChatOptions(openAiToken, url)
.azure(true)
.build())
.build();
}
private static OpenAiChatOptions.Builder buildOpenAiChatOptions(String apiKey, String url) {
OpenAiChatOptions.Builder optionsBuilder = OpenAiChatOptions.builder().apiKey(apiKey);
if (StrUtil.isNotEmpty(url)) {
optionsBuilder.baseUrl(url);
}
return optionsBuilder;
}
/**
* 可参考 {@link AnthropicChatAutoConfiguration} 的 anthropicApi 方法
*/
private static AnthropicChatModel buildAnthropicChatModel(String apiKey, String url) {
AnthropicApi.Builder builder = AnthropicApi.builder().apiKey(apiKey);
AnthropicChatOptions.Builder optionsBuilder = AnthropicChatOptions.builder().apiKey(apiKey);
if (StrUtil.isNotEmpty(url)) {
builder.baseUrl(url);
optionsBuilder.baseUrl(url);
}
AnthropicApi anthropicApi = builder.build();
return AnthropicChatModel.builder()
.anthropicApi(anthropicApi)
.toolCallingManager(getToolCallingManager())
.options(optionsBuilder.build())
.build();
}
/**
* 可参考 {@link AiAutoConfiguration#buildGeminiChatClient(YudaoAiProperties.Gemini)}
* 可参考 {@link GoogleGenAiChatAutoConfiguration} 的 googleGenAiChatModel 方法
*/
private static GeminiChatModel buildGeminiChatModel(String apiKey) {
YudaoAiProperties.Gemini properties = SpringUtil.getBean(YudaoAiProperties.class)
.getGemini().setApiKey(apiKey);
return new AiAutoConfiguration().buildGeminiChatClient(properties);
private static GoogleGenAiChatModel buildGeminiChatModel(String apiKey, String url) {
Client.Builder clientBuilder = Client.builder().apiKey(apiKey);
if (StrUtil.isNotBlank(url)) {
clientBuilder.httpOptions(HttpOptions.builder()
.baseUrl(url)
// TeamOrouter 的 Gemini 原生协议使用 Authorization Bearer 鉴权
.headers(Collections.singletonMap("Authorization", "Bearer " + apiKey))
.build());
}
return GoogleGenAiChatModel.builder()
.genAiClient(clientBuilder.build())
.options(GoogleGenAiChatOptions.builder()
.model("gemini-2.5-flash")
.build())
.toolCallingManager(SpringUtil.getBean(ToolCallingManager.class))
.retryTemplate(RetryUtils.DEFAULT_RETRY_TEMPLATE)
.observationRegistry(SpringUtil.getBean(ObservationRegistry.class))
.build();
}
/**
* 可参考 {@link OpenAiImageAutoConfiguration} 的 openAiImageModel 方法
*/
private OpenAiImageModel buildOpenAiImageModel(String openAiToken, String url) {
url = StrUtil.blankToDefault(url, OpenAiApiConstants.DEFAULT_BASE_URL);
OpenAiImageApi openAiApi = OpenAiImageApi.builder().baseUrl(url).apiKey(openAiToken).build();
return new OpenAiImageModel(openAiApi);
OpenAiImageOptions.Builder optionsBuilder = OpenAiImageOptions.builder().apiKey(openAiToken);
if (StrUtil.isNotEmpty(url)) {
optionsBuilder.baseUrl(url);
}
return OpenAiImageModel.builder()
.options(optionsBuilder.build())
.build();
}
/**
@ -577,7 +519,6 @@ public class AiModelFactoryImpl implements AiModelFactory {
OllamaApi ollamaApi = OllamaApi.builder().baseUrl(url).build();
return OllamaChatModel.builder()
.ollamaApi(ollamaApi)
.toolCallingManager(getToolCallingManager())
.build();
}
@ -600,47 +541,10 @@ public class AiModelFactoryImpl implements AiModelFactory {
// ========== 各种创建 EmbeddingModel 的方法 ==========
/**
* 可参考 {@link DashScopeEmbeddingAutoConfiguration} 的 dashscopeEmbeddingModel 方法
* 可参考 {@link DashScopeEmbeddingAutoConfiguration} 的 DashScopeEmbeddingModel 方法
*/
private DashScopeEmbeddingModel buildTongYiEmbeddingModel(String apiKey, String model) {
DashScopeApi dashScopeApi = DashScopeApi.builder().apiKey(apiKey).build();
DashScopeEmbeddingOptions dashScopeEmbeddingOptions = DashScopeEmbeddingOptions.builder().withModel(model).build();
return new DashScopeEmbeddingModel(dashScopeApi, MetadataMode.EMBED, dashScopeEmbeddingOptions);
}
/**
* 可参考 {@link ZhiPuAiEmbeddingAutoConfiguration} 的 zhiPuAiEmbeddingModel 方法
*/
private ZhiPuAiEmbeddingModel buildZhiPuEmbeddingModel(String apiKey, String url, String model) {
ZhiPuAiApi.Builder zhiPuAiApiBuilder = ZhiPuAiApi.builder().apiKey(apiKey);
if (StrUtil.isNotEmpty(url)) {
zhiPuAiApiBuilder.baseUrl(url);
}
ZhiPuAiEmbeddingOptions zhiPuAiEmbeddingOptions = ZhiPuAiEmbeddingOptions.builder().model(model).build();
return new ZhiPuAiEmbeddingModel(zhiPuAiApiBuilder.build(), MetadataMode.EMBED, zhiPuAiEmbeddingOptions);
}
/**
* 可参考 {@link MiniMaxEmbeddingAutoConfiguration} 的 miniMaxEmbeddingModel 方法
*/
private EmbeddingModel buildMiniMaxEmbeddingModel(String apiKey, String url, String model) {
MiniMaxApi miniMaxApi = StrUtil.isEmpty(url)? new MiniMaxApi(apiKey)
: new MiniMaxApi(url, apiKey);
MiniMaxEmbeddingOptions miniMaxEmbeddingOptions = MiniMaxEmbeddingOptions.builder().model(model).build();
return new MiniMaxEmbeddingModel(miniMaxApi, MetadataMode.EMBED, miniMaxEmbeddingOptions);
}
/**
* 可参考 {@link QianFanEmbeddingAutoConfiguration} 的 qianFanEmbeddingModel 方法
*/
private QianFanEmbeddingModel buildYiYanEmbeddingModel(String key, String model) {
List<String> keys = StrUtil.split(key, '|');
Assert.equals(keys.size(), 2, "YiYanChatClient 的密钥需要 (appKey|secretKey) 格式");
String appKey = keys.get(0);
String secretKey = keys.get(1);
QianFanApi qianFanApi = new QianFanApi(appKey, secretKey);
QianFanEmbeddingOptions qianFanEmbeddingOptions = QianFanEmbeddingOptions.builder().model(model).build();
return new QianFanEmbeddingModel(qianFanApi, MetadataMode.EMBED, qianFanEmbeddingOptions);
return AiAutoConfiguration.buildTongYiEmbeddingModel(apiKey, model);
}
private OllamaEmbeddingModel buildOllamaEmbeddingModel(String url, String model) {
@ -648,7 +552,7 @@ public class AiModelFactoryImpl implements AiModelFactory {
OllamaEmbeddingOptions ollamaOptions = OllamaEmbeddingOptions.builder().model(model).build();
return OllamaEmbeddingModel.builder()
.ollamaApi(ollamaApi)
.defaultOptions(ollamaOptions)
.options(ollamaOptions)
.build();
}
@ -656,25 +560,31 @@ public class AiModelFactoryImpl implements AiModelFactory {
* 可参考 {@link OpenAiEmbeddingAutoConfiguration} 的 openAiEmbeddingModel 方法
*/
private OpenAiEmbeddingModel buildOpenAiEmbeddingModel(String openAiToken, String url, String model) {
url = StrUtil.blankToDefault(url, OpenAiApiConstants.DEFAULT_BASE_URL);
OpenAiApi openAiApi = OpenAiApi.builder().baseUrl(url).apiKey(openAiToken).build();
OpenAiEmbeddingOptions openAiEmbeddingProperties = OpenAiEmbeddingOptions.builder().model(model).build();
return new OpenAiEmbeddingModel(openAiApi, MetadataMode.EMBED, openAiEmbeddingProperties);
OpenAiEmbeddingOptions.Builder optionsBuilder = OpenAiEmbeddingOptions.builder()
.apiKey(openAiToken)
.model(model);
if (StrUtil.isNotEmpty(url)) {
optionsBuilder.baseUrl(url);
}
return OpenAiEmbeddingModel.builder()
.metadataMode(MetadataMode.EMBED)
.options(optionsBuilder.build())
.build();
}
/**
* 可参考 {@link AzureOpenAiEmbeddingAutoConfiguration} 的 azureOpenAiEmbeddingModel 方法
*/
private AzureOpenAiEmbeddingModel buildAzureOpenAiEmbeddingModel(String apiKey, String url, String model) {
// TODO @芋艿:手头暂时没密钥,使用建议再测试下
AzureOpenAiEmbeddingAutoConfiguration azureOpenAiAutoConfiguration = new AzureOpenAiEmbeddingAutoConfiguration();
// 创建 OpenAIClientBuilder 对象
OpenAIClientBuilder openAIClientBuilder = new OpenAIClientBuilder()
.endpoint(url).credential(new KeyCredential(apiKey));
// 获取 AzureOpenAiChatProperties 对象
AzureOpenAiEmbeddingProperties embeddingProperties = SpringUtil.getBean(AzureOpenAiEmbeddingProperties.class);
return azureOpenAiAutoConfiguration.azureOpenAiEmbeddingModel(openAIClientBuilder, embeddingProperties,
getObservationRegistry(), getEmbeddingModelObservationConvention());
private OpenAiEmbeddingModel buildAzureOpenAiEmbeddingModel(String openAiToken, String url, String model) {
OpenAiEmbeddingOptions.Builder optionsBuilder = OpenAiEmbeddingOptions.builder()
.apiKey(openAiToken)
.model(model)
.deploymentName(model)
.azure(true);
if (StrUtil.isNotEmpty(url)) {
optionsBuilder.baseUrl(url);
}
return OpenAiEmbeddingModel.builder()
.metadataMode(MetadataMode.EMBED)
.options(optionsBuilder.build())
.build();
}
// ========== 各种创建 VectorStore 的方法 ==========
@ -737,13 +647,11 @@ public class AiModelFactoryImpl implements AiModelFactory {
*/
private RedisVectorStore buildRedisVectorStore(EmbeddingModel embeddingModel,
Map<String, Class<?>> metadataFields) {
// 创建 JedisPooled 对象
RedisProperties redisProperties = SpringUtils.getBean(RedisProperties.class);
JedisPooled jedisPooled = new JedisPooled(redisProperties.getHost(), redisProperties.getPort(),
redisProperties.getUsername(), redisProperties.getPassword());
// 创建 RedisClient 对象
RedisClient redisClient = buildRedisClient();
// 创建 RedisVectorStoreProperties 对象
RedisVectorStoreProperties properties = SpringUtil.getBean(RedisVectorStoreProperties.class);
RedisVectorStore redisVectorStore = RedisVectorStore.builder(jedisPooled, embeddingModel)
RedisVectorStore redisVectorStore = RedisVectorStore.builder(redisClient, embeddingModel)
.indexName(properties.getIndexName()).prefix(properties.getPrefix())
.initializeSchema(properties.isInitializeSchema())
.metadataFields(convertList(metadataFields.entrySet(), entry -> {
@ -766,6 +674,43 @@ public class AiModelFactoryImpl implements AiModelFactory {
return redisVectorStore;
}
private RedisClient buildRedisClient() {
DataRedisProperties redisProperties = SpringUtil.getBean(DataRedisProperties.class);
Assert.isNull(redisProperties.getCluster(), "RedisVectorStore 暂不支持 Redis Cluster 模式");
Assert.isNull(redisProperties.getSentinel(), "RedisVectorStore 暂不支持 Redis Sentinel 模式");
Assert.isNull(redisProperties.getMasterreplica(), "RedisVectorStore 暂不支持 Redis Master-Replica 模式");
if (StrUtil.isNotEmpty(redisProperties.getUrl())) {
return RedisClient.create(redisProperties.getUrl());
}
DefaultJedisClientConfig.Builder clientConfigBuilder = DefaultJedisClientConfig.builder()
.ssl(redisProperties.getSsl().isEnabled())
.database(redisProperties.getDatabase());
if (StrUtil.isNotEmpty(redisProperties.getUsername())) {
clientConfigBuilder.user(redisProperties.getUsername());
}
if (StrUtil.isNotEmpty(redisProperties.getPassword())) {
clientConfigBuilder.password(redisProperties.getPassword());
}
if (StrUtil.isNotEmpty(redisProperties.getClientName())) {
clientConfigBuilder.clientName(redisProperties.getClientName());
}
if (redisProperties.getTimeout() != null) {
clientConfigBuilder.socketTimeoutMillis(toMillis(redisProperties.getTimeout()));
}
if (redisProperties.getConnectTimeout() != null) {
clientConfigBuilder.connectionTimeoutMillis(toMillis(redisProperties.getConnectTimeout()));
}
JedisClientConfig clientConfig = clientConfigBuilder.build();
return RedisClient.builder()
.hostAndPort(new HostAndPort(redisProperties.getHost(), redisProperties.getPort()))
.clientConfig(clientConfig)
.build();
}
private static int toMillis(Duration duration) {
return Math.toIntExact(duration.toMillis());
}
/**
* 参考 {@link MilvusVectorStoreAutoConfiguration} 的 vectorStore 方法
*/
@ -827,10 +772,6 @@ public class AiModelFactoryImpl implements AiModelFactory {
return SpringUtil.getBean(BatchingStrategy.class);
}
private static ToolCallingManager getToolCallingManager() {
return SpringUtil.getBean(ToolCallingManager.class);
}
private static ObjectProvider<EmbeddingModelObservationConvention> getEmbeddingModelObservationConvention() {
return new ObjectProvider<>() {

View File

@ -6,7 +6,7 @@ import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.ai.deepseek.DeepSeekChatModel;
import reactor.core.publisher.Flux;
/**
@ -20,26 +20,35 @@ public class BaiChuanChatModel implements ChatModel {
public static final String BASE_URL = "https://api.baichuan-ai.com";
public static final String MODEL_DEFAULT = "Baichuan4-Turbo";
public static final String COMPLETE_PATH = "/v1/chat/completions";
public static final String MODEL_DEFAULT = "Baichuan-M3";
/**
* 兼容 OpenAI 接口,进行复用
* 兼容 OpenAI 接口,复用 DeepSeek 客户端
*/
private final OpenAiChatModel openAiChatModel;
private final DeepSeekChatModel deepSeekChatModel;
@Override
public ChatResponse call(Prompt prompt) {
return openAiChatModel.call(prompt);
return deepSeekChatModel.call(prompt);
}
@Override
public Flux<ChatResponse> stream(Prompt prompt) {
return openAiChatModel.stream(prompt);
return deepSeekChatModel.stream(prompt);
}
@Override
public ChatOptions getOptions() {
return deepSeekChatModel.getOptions();
}
@Override
@Deprecated(forRemoval = true)
@SuppressWarnings("removal")
public ChatOptions getDefaultOptions() {
return openAiChatModel.getDefaultOptions();
return getOptions();
}
}

View File

@ -20,7 +20,7 @@ public class DouBaoChatModel implements ChatModel {
public static final String BASE_URL = "https://ark.cn-beijing.volces.com/api";
public static final String COMPLETE_PATH = "/v3/chat/completions";
public static final String MODEL_DEFAULT = "doubao-1-5-lite-32k-250115";
public static final String MODEL_DEFAULT = "doubao-seed-2-1-turbo-260628";
/**
* 兼容 OpenAI 接口,进行复用
@ -38,8 +38,15 @@ public class DouBaoChatModel implements ChatModel {
}
@Override
public ChatOptions getOptions() {
return openAiChatModel.getOptions();
}
@Override
@Deprecated(forRemoval = true)
@SuppressWarnings("removal")
public ChatOptions getDefaultOptions() {
return openAiChatModel.getDefaultOptions();
return getOptions();
}
}

View File

@ -1,46 +0,0 @@
package cn.iocoder.yudao.module.ai.framework.ai.core.model.gemini;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.openai.OpenAiChatModel;
import reactor.core.publisher.Flux;
/**
* 谷歌 Gemini {@link ChatModel} 实现类,基于 Google AI Studio 提供的 <a href="https://ai.google.dev/gemini-api/docs/openai">OpenAI 兼容方案</a>
*
* @author 芋道源码
*/
@Slf4j
@RequiredArgsConstructor
public class GeminiChatModel implements ChatModel {
public static final String BASE_URL = "https://generativelanguage.googleapis.com/v1beta/openai/";
public static final String COMPLETE_PATH = "/chat/completions";
public static final String MODEL_DEFAULT = "gemini-2.5-flash";
/**
* 兼容 OpenAI 接口,进行复用
*/
private final OpenAiChatModel openAiChatModel;
@Override
public ChatResponse call(Prompt prompt) {
return openAiChatModel.call(prompt);
}
@Override
public Flux<ChatResponse> stream(Prompt prompt) {
return openAiChatModel.stream(prompt);
}
@Override
public ChatOptions getDefaultOptions() {
return openAiChatModel.getDefaultOptions();
}
}

View File

@ -17,9 +17,9 @@ import reactor.core.publisher.Flux;
@RequiredArgsConstructor
public class GrokChatModel implements ChatModel {
public static final String BASE_URL = "https://api.x.ai";
public static final String BASE_URL = "https://api.x.ai/v1";
public static final String COMPLETE_PATH = "/v1/chat/completions";
public static final String MODEL_DEFAULT = "grok-4-fast-reasoning";
public static final String MODEL_DEFAULT = "grok-4.3";
/**
* 兼容 OpenAI 接口,进行复用
@ -37,8 +37,15 @@ public class GrokChatModel implements ChatModel {
}
@Override
public ChatOptions getOptions() {
return openAiChatModel.getOptions();
}
@Override
@Deprecated(forRemoval = true)
@SuppressWarnings("removal")
public ChatOptions getDefaultOptions() {
return openAiChatModel.getDefaultOptions();
return getOptions();
}
}

View File

@ -9,10 +9,9 @@ import org.springframework.ai.chat.prompt.Prompt;
import reactor.core.publisher.Flux;
/**
* 腾混元 {@link ChatModel} 实现类
* 腾混元 {@link ChatModel} 实现类
*
* 1. 混元大模型:基于 <a href="https://cloud.tencent.com/document/product/1729/111007">知识引擎原子能力</a> 实现
* 2. 知识引擎原子能力:基于 <a href="https://cloud.tencent.com/document/product/1772/115969">知识引擎原子能力</a> 实现
* 基于 <a href="https://cloud.tencent.com/document/product/1823/132252">TokenHub OpenAI 兼容接口</a> 实现
*
* @author fansili
*/
@ -20,14 +19,10 @@ import reactor.core.publisher.Flux;
@RequiredArgsConstructor
public class HunYuanChatModel implements ChatModel {
public static final String BASE_URL = "https://api.hunyuan.cloud.tencent.com";
public static final String BASE_URL = "https://tokenhub.tencentmaas.com";
public static final String COMPLETE_PATH = "/v1/chat/completions";
public static final String MODEL_DEFAULT = "hunyuan-turbo";
public static final String DEEP_SEEK_BASE_URL = "https://api.lkeap.cloud.tencent.com";
public static final String DEEP_SEEK_MODEL_DEFAULT = "deepseek-v3";
public static final String MODEL_DEFAULT = "hy3-preview";
/**
* 兼容 OpenAI 接口,进行复用
@ -45,8 +40,15 @@ public class HunYuanChatModel implements ChatModel {
}
@Override
public ChatOptions getOptions() {
return openAiChatModel.getOptions();
}
@Override
@Deprecated(forRemoval = true)
@SuppressWarnings("removal")
public ChatOptions getDefaultOptions() {
return openAiChatModel.getDefaultOptions();
return getOptions();
}
}

View File

@ -0,0 +1,52 @@
package cn.iocoder.yudao.module.ai.framework.ai.core.model.minimax;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.deepseek.DeepSeekChatModel;
import reactor.core.publisher.Flux;
/**
* MiniMax {@link ChatModel} 实现类
*
* @author 芋道源码
*/
@Slf4j
@RequiredArgsConstructor
public class MiniMaxChatModel implements ChatModel {
public static final String BASE_URL = "https://api.minimaxi.com/v1";
public static final String MODEL_DEFAULT = "MiniMax-M3";
/**
* 兼容 OpenAI 接口,复用 DeepSeek 客户端
*/
private final DeepSeekChatModel deepSeekChatModel;
@Override
public ChatResponse call(Prompt prompt) {
return deepSeekChatModel.call(prompt);
}
@Override
public Flux<ChatResponse> stream(Prompt prompt) {
return deepSeekChatModel.stream(prompt);
}
@Override
public ChatOptions getOptions() {
return deepSeekChatModel.getOptions();
}
@Override
@Deprecated(forRemoval = true)
@SuppressWarnings("removal")
public ChatOptions getDefaultOptions() {
return getOptions();
}
}

View File

@ -0,0 +1,54 @@
package cn.iocoder.yudao.module.ai.framework.ai.core.model.moonshot;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.deepseek.DeepSeekChatModel;
import reactor.core.publisher.Flux;
/**
* 月之暗面 {@link ChatModel} 实现类
*
* @author 芋道源码
*/
@Slf4j
@RequiredArgsConstructor
public class MoonshotChatModel implements ChatModel {
public static final String BASE_URL = "https://api.moonshot.cn";
public static final String COMPLETE_PATH = "/v1/chat/completions";
public static final String MODEL_DEFAULT = "kimi-k2.6";
/**
* 兼容 OpenAI 接口,复用 DeepSeek 客户端
*/
private final DeepSeekChatModel deepSeekChatModel;
@Override
public ChatResponse call(Prompt prompt) {
return deepSeekChatModel.call(prompt);
}
@Override
public Flux<ChatResponse> stream(Prompt prompt) {
return deepSeekChatModel.stream(prompt);
}
@Override
public ChatOptions getOptions() {
return deepSeekChatModel.getOptions();
}
@Override
@Deprecated(forRemoval = true)
@SuppressWarnings("removal")
public ChatOptions getDefaultOptions() {
return getOptions();
}
}

View File

@ -25,7 +25,7 @@ public final class SiliconFlowApiConstants {
public static final String DEFAULT_BASE_URL = "https://api.siliconflow.cn";
public static final String MODEL_DEFAULT = "deepseek-ai/DeepSeek-R1-Distill-Qwen-7B";
public static final String MODEL_DEFAULT = "deepseek-ai/DeepSeek-V4-Pro";
public static final String DEFAULT_IMAGE_MODEL = "Kwai-Kolors/Kolors";

View File

@ -6,7 +6,6 @@ import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.openai.OpenAiChatModel;
import reactor.core.publisher.Flux;
/**
@ -36,8 +35,15 @@ public class SiliconFlowChatModel implements ChatModel {
}
@Override
public ChatOptions getOptions() {
return openAiChatModel.getOptions();
}
@Override
@Deprecated(forRemoval = true)
@SuppressWarnings("removal")
public ChatOptions getDefaultOptions() {
return openAiChatModel.getDefaultOptions();
return getOptions();
}
}

View File

@ -21,18 +21,14 @@ import com.fasterxml.jackson.annotation.JsonProperty;
import org.springframework.ai.model.ApiKey;
import org.springframework.ai.model.NoopApiKey;
import org.springframework.ai.model.SimpleApiKey;
import org.springframework.ai.openai.api.OpenAiImageApi;
import org.springframework.ai.retry.RetryUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.ResponseErrorHandler;
import org.springframework.web.client.RestClient;
import java.util.Map;
/**
* 硅基流动 Image API
*
@ -58,15 +54,15 @@ public class SiliconFlowImageApi {
public SiliconFlowImageApi(String baseUrl, String apiKey, RestClient.Builder restClientBuilder,
ResponseErrorHandler responseErrorHandler) {
this(baseUrl, apiKey, CollectionUtils.toMultiValueMap(Map.of()), restClientBuilder, responseErrorHandler);
this(baseUrl, apiKey, new HttpHeaders(), restClientBuilder, responseErrorHandler);
}
public SiliconFlowImageApi(String baseUrl, String apiKey, MultiValueMap<String, String> headers,
public SiliconFlowImageApi(String baseUrl, String apiKey, HttpHeaders headers,
RestClient.Builder restClientBuilder, ResponseErrorHandler responseErrorHandler) {
this(baseUrl, new SimpleApiKey(apiKey), headers, restClientBuilder, responseErrorHandler);
}
public SiliconFlowImageApi(String baseUrl, ApiKey apiKey, MultiValueMap<String, String> headers,
public SiliconFlowImageApi(String baseUrl, ApiKey apiKey, HttpHeaders headers,
RestClient.Builder restClientBuilder, ResponseErrorHandler responseErrorHandler) {
// @formatter:off
@ -83,7 +79,7 @@ public class SiliconFlowImageApi {
// @formatter:on
}
public ResponseEntity<OpenAiImageApi.OpenAiImageResponse> createImage(SiliconflowImageRequest siliconflowImageRequest) {
public ResponseEntity<SiliconFlowImageResponse> createImage(SiliconflowImageRequest siliconflowImageRequest) {
Assert.notNull(siliconflowImageRequest, "Image request cannot be null.");
Assert.hasLength(siliconflowImageRequest.prompt(), "Prompt cannot be empty.");
@ -91,7 +87,7 @@ public class SiliconFlowImageApi {
.uri("v1/images/generations")
.body(siliconflowImageRequest)
.retrieve()
.toEntity(OpenAiImageApi.OpenAiImageResponse.class);
.toEntity(SiliconFlowImageResponse.class);
}
@ -112,4 +108,15 @@ public class SiliconFlowImageApi {
}
}
public record SiliconFlowImageResponse(
@JsonProperty("created") Long created,
@JsonProperty("data") java.util.List<Entry> data) {
public record Entry(
@JsonProperty("url") String url,
@JsonProperty("b64_json") String b64Json,
@JsonProperty("revised_prompt") String revisedPrompt) {
}
}
}

View File

@ -27,12 +27,11 @@ import org.springframework.ai.image.observation.ImageModelObservationConvention;
import org.springframework.ai.image.observation.ImageModelObservationDocumentation;
import org.springframework.ai.model.ModelOptionsUtils;
import org.springframework.ai.openai.OpenAiImageModel;
import org.springframework.ai.openai.api.OpenAiImageApi;
import org.springframework.ai.openai.metadata.OpenAiImageGenerationMetadata;
import org.springframework.ai.retry.RetryUtils;
import org.springframework.core.retry.RetryTemplate;
import org.springframework.http.ResponseEntity;
import org.springframework.lang.Nullable;
import org.springframework.retry.support.RetryTemplate;
import org.jspecify.annotations.Nullable;
import org.springframework.util.Assert;
import java.util.List;
@ -71,7 +70,7 @@ public class SiliconFlowImageModel implements ImageModel {
public SiliconFlowImageModel(SiliconFlowImageApi siliconFlowImageApi, SiliconFlowImageOptions options, RetryTemplate retryTemplate,
ObservationRegistry observationRegistry) {
Assert.notNull(siliconFlowImageApi, "OpenAiImageApi must not be null");
Assert.notNull(siliconFlowImageApi, "SiliconFlowImageApi must not be null");
Assert.notNull(options, "options must not be null");
Assert.notNull(retryTemplate, "retryTemplate must not be null");
Assert.notNull(observationRegistry, "observationRegistry must not be null");
@ -96,8 +95,8 @@ public class SiliconFlowImageModel implements ImageModel {
.observation(this.observationConvention, DEFAULT_OBSERVATION_CONVENTION, () -> observationContext,
this.observationRegistry)
.observe(() -> {
ResponseEntity<OpenAiImageApi.OpenAiImageResponse> imageResponseEntity = this.retryTemplate
.execute(ctx -> this.siliconFlowImageApi.createImage(imageRequest));
ResponseEntity<SiliconFlowImageApi.SiliconFlowImageResponse> imageResponseEntity = RetryUtils.execute(
this.retryTemplate, () -> this.siliconFlowImageApi.createImage(imageRequest));
ImageResponse imageResponse = convertResponse(imageResponseEntity, imageRequest);
@ -109,17 +108,22 @@ public class SiliconFlowImageModel implements ImageModel {
private SiliconFlowImageApi.SiliconflowImageRequest createRequest(ImagePrompt imagePrompt,
SiliconFlowImageOptions requestImageOptions) {
String instructions = imagePrompt.getInstructions().get(0).getText();
String instructions = imagePrompt.getInstructions().getFirst().getText();
SiliconFlowImageApi.SiliconflowImageRequest imageRequest = new SiliconFlowImageApi.SiliconflowImageRequest(instructions,
SiliconFlowApiConstants.DEFAULT_IMAGE_MODEL);
return ModelOptionsUtils.merge(requestImageOptions, imageRequest, SiliconFlowImageApi.SiliconflowImageRequest.class);
return new SiliconFlowImageApi.SiliconflowImageRequest(
instructions,
ModelOptionsUtils.mergeOption(requestImageOptions.getModel(), SiliconFlowApiConstants.DEFAULT_IMAGE_MODEL),
requestImageOptions.getN(),
requestImageOptions.getNegativePrompt(),
requestImageOptions.getSeed(),
requestImageOptions.getNumInferenceSteps(),
requestImageOptions.getGuidanceScale(),
requestImageOptions.getImage());
}
private ImageResponse convertResponse(ResponseEntity<OpenAiImageApi.OpenAiImageResponse> imageResponseEntity,
private ImageResponse convertResponse(ResponseEntity<SiliconFlowImageApi.SiliconFlowImageResponse> imageResponseEntity,
SiliconFlowImageApi.SiliconflowImageRequest siliconflowImageRequest) {
OpenAiImageApi.OpenAiImageResponse imageApiResponse = imageResponseEntity.getBody();
SiliconFlowImageApi.SiliconFlowImageResponse imageApiResponse = imageResponseEntity.getBody();
if (imageApiResponse == null) {
logger.warn("No image response returned for request: {}", siliconflowImageRequest);
return new ImageResponse(List.of());
@ -136,12 +140,17 @@ public class SiliconFlowImageModel implements ImageModel {
}
private SiliconFlowImageOptions mergeOptions(@Nullable ImageOptions runtimeOptions, SiliconFlowImageOptions defaultOptions) {
var runtimeOptionsForProvider = ModelOptionsUtils.copyToTarget(runtimeOptions, ImageOptions.class,
SiliconFlowImageOptions.class);
if (runtimeOptionsForProvider == null) {
if (runtimeOptions == null) {
return defaultOptions;
}
SiliconFlowImageOptions runtimeOptionsForProvider = runtimeOptions instanceof SiliconFlowImageOptions siliconFlowImageOptions
? siliconFlowImageOptions
: SiliconFlowImageOptions.builder()
.model(runtimeOptions.getModel())
.batchSize(runtimeOptions.getN())
.width(runtimeOptions.getWidth())
.height(runtimeOptions.getHeight())
.build();
return SiliconFlowImageOptions.builder()
// Handle portable image options

View File

@ -0,0 +1,54 @@
package cn.iocoder.yudao.module.ai.framework.ai.core.model.stepfun;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.deepseek.DeepSeekChatModel;
import reactor.core.publisher.Flux;
/**
* 阶跃星辰 {@link ChatModel} 实现类
*
* @author 芋道源码
*/
@Slf4j
@RequiredArgsConstructor
public class StepFunChatModel implements ChatModel {
public static final String BASE_URL = "https://api.stepfun.com";
public static final String COMPLETE_PATH = "/v1/chat/completions";
public static final String MODEL_DEFAULT = "step-3.7-flash";
/**
* 兼容 OpenAI 接口,复用 DeepSeek 客户端
*/
private final DeepSeekChatModel deepSeekChatModel;
@Override
public ChatResponse call(Prompt prompt) {
return deepSeekChatModel.call(prompt);
}
@Override
public Flux<ChatResponse> stream(Prompt prompt) {
return deepSeekChatModel.stream(prompt);
}
@Override
public ChatOptions getOptions() {
return deepSeekChatModel.getOptions();
}
@Override
@Deprecated(forRemoval = true)
@SuppressWarnings("removal")
public ChatOptions getDefaultOptions() {
return getOptions();
}
}

View File

@ -1,11 +1,14 @@
package cn.iocoder.yudao.module.ai.framework.ai.core.model.xinghuo;
import lombok.RequiredArgsConstructor;
import cn.hutool.core.util.StrUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.deepseek.DeepSeekChatModel;
import org.springframework.ai.deepseek.DeepSeekChatOptions;
import org.springframework.ai.deepseek.api.DeepSeekApi;
import reactor.core.publisher.Flux;
/**
@ -14,37 +17,144 @@ import reactor.core.publisher.Flux;
* @author fansili
*/
@Slf4j
@RequiredArgsConstructor
public class XingHuoChatModel implements ChatModel {
public static final String BASE_URL_V1 = "https://spark-api-open.xf-yun.com";
public static final String BASE_URL_V2 = "https://spark-api-open.xf-yun.com";
public static final String BASE_COMPLETIONS_PATH_V2 = "/v2/chat/completions";
/**
* 星火 X2
*
* @see <a href="https://spark-api-open.xf-yun.com/x2/chat/completions">接口地址</a>
*/
public static final String MODEL_X2 = "x2";
/**
* 已知模型名列表x1、4.0Ultra、generalv3.5、max-32k、generalv3、pro-128k、lite
* 星火 X2 Flash
*
* @see <a href="https://spark-api-open.xf-yun.com/agent/v1/chat/completions">接口地址</a>
*/
public static final String MODEL_DEFAULT = "4.0Ultra";
public static final String MODEL_X2_FLASH = "x2-flash";
private static final String BASE_URL_X2 = "https://spark-api-open.xf-yun.com/x2";
private static final String BASE_URL_X2_FLASH = "https://spark-api-open.xf-yun.com/agent/v1";
public static final String MODEL_DEFAULT = MODEL_X2_FLASH;
private static String getBaseUrl(String model) {
if (MODEL_X2_FLASH.equals(model)) {
return BASE_URL_X2_FLASH;
}
return BASE_URL_X2;
}
private final String apiKey;
private final DeepSeekChatOptions options;
/**
* v1 兼容 OpenAI 接口,进行复用
* 兼容 OpenAI 接口,进行复用
*/
private final ChatModel openAiChatModelV1;
private final ChatModel chatModelX2;
private final ChatModel chatModelX2Flash;
private XingHuoChatModel(String apiKey, DeepSeekChatOptions options) {
this.apiKey = apiKey;
this.options = options;
this.chatModelX2 = buildChatModel(MODEL_X2);
this.chatModelX2Flash = buildChatModel(MODEL_X2_FLASH);
}
public static Builder builder() {
return new Builder();
}
@Override
public ChatResponse call(Prompt prompt) {
return openAiChatModelV1.call(prompt);
return getChatModel(prompt).call(buildApiPrompt(prompt));
}
@Override
public Flux<ChatResponse> stream(Prompt prompt) {
return openAiChatModelV1.stream(prompt);
return getChatModel(prompt).stream(buildApiPrompt(prompt));
}
@Override
public ChatOptions getOptions() {
return options;
}
@Override
@Deprecated(forRemoval = true)
@SuppressWarnings("removal")
public ChatOptions getDefaultOptions() {
return openAiChatModelV1.getDefaultOptions();
return getOptions();
}
private ChatModel getChatModel(Prompt prompt) {
String model = options.getModel();
ChatOptions options = prompt.getOptions();
if (options != null && isBusinessModel(options.getModel())) {
model = options.getModel();
}
return getChatModel(model);
}
private ChatModel getChatModel(String model) {
if (MODEL_X2_FLASH.equals(model)) {
return chatModelX2Flash;
}
return chatModelX2;
}
private ChatModel buildChatModel(String model) {
return DeepSeekChatModel.builder()
.deepSeekApi(DeepSeekApi.builder()
.baseUrl(getBaseUrl(model))
.apiKey(apiKey)
.build())
.options(options.mutate().model("spark-x").build())
.build();
}
private static Prompt buildApiPrompt(Prompt prompt) {
ChatOptions options = prompt.getOptions();
if (options == null) {
return prompt;
}
return Prompt.builder()
.messages(prompt.getInstructions())
.chatOptions(options.mutate().model("spark-x").build())
.build();
}
private static boolean isBusinessModel(String model) {
return MODEL_X2.equals(model) || MODEL_X2_FLASH.equals(model);
}
public static final class Builder {
private String apiKey;
private DeepSeekChatOptions options;
public Builder apiKey(String apiKey) {
this.apiKey = apiKey;
return this;
}
public Builder options(DeepSeekChatOptions options) {
this.options = options;
return this;
}
public XingHuoChatModel build() {
DeepSeekChatOptions options = this.options != null ? this.options : DeepSeekChatOptions.builder().build();
if (StrUtil.isEmpty(options.getModel())) {
options = options.mutate().model(MODEL_DEFAULT).build();
}
return new XingHuoChatModel(apiKey, options);
}
}
}

View File

@ -0,0 +1,52 @@
package cn.iocoder.yudao.module.ai.framework.ai.core.model.yiyan;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.deepseek.DeepSeekChatModel;
import reactor.core.publisher.Flux;
/**
* 文心一言 {@link ChatModel} 实现类
*
* @author 芋道源码
*/
@Slf4j
@RequiredArgsConstructor
public class YiYanChatModel implements ChatModel {
public static final String BASE_URL = "https://qianfan.baidubce.com/v2";
public static final String MODEL_DEFAULT = "ernie-5.1";
/**
* 兼容 OpenAI 接口,复用 DeepSeek 客户端
*/
private final DeepSeekChatModel deepSeekChatModel;
@Override
public ChatResponse call(Prompt prompt) {
return deepSeekChatModel.call(prompt);
}
@Override
public Flux<ChatResponse> stream(Prompt prompt) {
return deepSeekChatModel.stream(prompt);
}
@Override
public ChatOptions getOptions() {
return deepSeekChatModel.getOptions();
}
@Override
@Deprecated(forRemoval = true)
@SuppressWarnings("removal")
public ChatOptions getDefaultOptions() {
return getOptions();
}
}

View File

@ -0,0 +1,52 @@
package cn.iocoder.yudao.module.ai.framework.ai.core.model.zhipu;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.deepseek.DeepSeekChatModel;
import reactor.core.publisher.Flux;
/**
* 智谱 {@link ChatModel} 实现类
*
* @author 芋道源码
*/
@Slf4j
@RequiredArgsConstructor
public class ZhiPuChatModel implements ChatModel {
public static final String BASE_URL = "https://open.bigmodel.cn/api/paas/v4";
public static final String MODEL_DEFAULT = "glm-5.2";
/**
* 兼容 OpenAI 接口,复用 DeepSeek 客户端
*/
private final DeepSeekChatModel deepSeekChatModel;
@Override
public ChatResponse call(Prompt prompt) {
return deepSeekChatModel.call(prompt);
}
@Override
public Flux<ChatResponse> stream(Prompt prompt) {
return deepSeekChatModel.stream(prompt);
}
@Override
public ChatOptions getOptions() {
return deepSeekChatModel.getOptions();
}
@Override
@Deprecated(forRemoval = true)
@SuppressWarnings("removal")
public ChatOptions getDefaultOptions() {
return getOptions();
}
}

View File

@ -1,26 +1,25 @@
package cn.iocoder.yudao.module.ai.framework.security.config;
import cn.iocoder.yudao.framework.security.config.AuthorizeRequestsCustomizer;
import jakarta.annotation.Resource;
import org.springframework.ai.mcp.server.common.autoconfigure.properties.McpServerSseProperties;
import org.springframework.ai.mcp.server.common.autoconfigure.properties.McpServerStreamableHttpProperties;
import cn.hutool.core.util.StrUtil;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer;
import java.util.Optional;
/**
* AI 模块的 Security 配置
*/
@Configuration(proxyBeanMethods = false, value = "aiSecurityConfiguration")
public class SecurityConfiguration {
@Resource
private Optional<McpServerSseProperties> mcpServerSseProperties;
@Resource
private Optional<McpServerStreamableHttpProperties> mcpServerStreamableHttpProperties;
@Value("${spring.ai.mcp.server.sse-endpoint:/sse}")
private String mcpSseEndpoint;
@Value("${spring.ai.mcp.server.sse-message-endpoint:/mcp/message}")
private String mcpSseMessageEndpoint;
@Value("${spring.ai.mcp.server.streamable-http-endpoint:/mcp}")
private String mcpStreamableHttpEndpoint;
@Bean("aiAuthorizeRequestsCustomizer")
public AuthorizeRequestsCustomizer authorizeRequestsCustomizer() {
@ -28,12 +27,15 @@ public class SecurityConfiguration {
@Override
public void customize(AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry registry) {
mcpServerSseProperties.ifPresent(properties -> {
registry.requestMatchers(properties.getSseEndpoint()).permitAll();
registry.requestMatchers(properties.getSseMessageEndpoint()).permitAll();
});
mcpServerStreamableHttpProperties.ifPresent(properties ->
registry.requestMatchers(properties.getMcpEndpoint()).permitAll());
if (StrUtil.isNotBlank(mcpSseEndpoint)) {
registry.requestMatchers(mcpSseEndpoint).permitAll();
}
if (StrUtil.isNotBlank(mcpSseMessageEndpoint)) {
registry.requestMatchers(mcpSseMessageEndpoint).permitAll();
}
if (StrUtil.isNotBlank(mcpStreamableHttpEndpoint)) {
registry.requestMatchers(mcpStreamableHttpEndpoint).permitAll();
}
}
};

View File

@ -49,10 +49,10 @@ import org.springframework.ai.chat.model.StreamingChatModel;
import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.mcp.SyncMcpToolCallbackProvider;
import org.springframework.ai.mcp.client.common.autoconfigure.properties.McpClientCommonProperties;
import org.springframework.ai.tool.ToolCallback;
import org.springframework.ai.tool.resolution.ToolCallbackResolver;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import reactor.core.publisher.Flux;
@ -130,9 +130,8 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
@Autowired(required = false) // 由于 yudao.ai.mcp.client.enable 配置项,可以关闭 McpSyncClient 的功能,所以这里只能不强制注入
private List<McpSyncClient> mcpClients;
@SuppressWarnings("SpringJavaAutowiredFieldsWarningInspection")
@Autowired(required = false) // 由于 yudao.ai.mcp.client.enable 配置项,可以关闭 McpSyncClient 的功能,所以这里只能不强制注入
private McpClientCommonProperties mcpClientCommonProperties;
@Value("${spring.ai.mcp.client.name:mcp}")
private String mcpClientName;
@Resource
private ToolCallbackResolver toolCallbackResolver;
@ -410,13 +409,16 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
if (CollUtil.isNotEmpty(mcpClients) && CollUtil.isNotEmpty(chatRole.getMcpClientNames())) {
chatRole.getMcpClientNames().forEach(mcpClientName -> {
// 2.1 标准化名字,参考 McpClientAutoConfiguration 的 connectedClientName 方法
String finalMcpClientName = mcpClientCommonProperties.getName() + " - " + mcpClientName;
String finalMcpClientName = this.mcpClientName + " - " + mcpClientName;
// 2.2 匹配对应的 McpSyncClient
mcpClients.forEach(mcpClient -> {
if (ObjUtil.notEqual(mcpClient.getClientInfo().name(), finalMcpClientName)) {
return;
}
ToolCallback[] mcpToolCallBacks = new SyncMcpToolCallbackProvider(mcpClient).getToolCallbacks();
ToolCallback[] mcpToolCallBacks = SyncMcpToolCallbackProvider.builder()
.mcpClients(mcpClient)
.build()
.getToolCallbacks();
CollUtil.addAll(toolCallbacks, mcpToolCallBacks);
});
});
@ -539,7 +541,7 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
public void deleteChatMessageByConversationId(Long conversationId, Long userId) {
// 1. 校验消息存在
List<AiChatMessageDO> messages = chatMessageMapper.selectListByConversationId(conversationId);
if (CollUtil.isEmpty(messages) || ObjUtil.notEqual(messages.get(0).getUserId(), userId)) {
if (CollUtil.isEmpty(messages) || ObjUtil.notEqual(messages.getFirst().getUserId(), userId)) {
throw exception(CHAT_MESSAGE_NOT_EXIST);
}
// 2. 执行删除

View File

@ -29,14 +29,12 @@ import cn.iocoder.yudao.module.infra.api.file.FileApi;
import com.alibaba.cloud.ai.dashscope.image.DashScopeImageOptions;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springaicommunity.qianfan.QianFanImageOptions;
import org.springframework.ai.image.ImageModel;
import org.springframework.ai.image.ImageOptions;
import org.springframework.ai.image.ImagePrompt;
import org.springframework.ai.image.ImageResponse;
import org.springframework.ai.openai.OpenAiImageOptions;
import org.springframework.ai.stabilityai.api.StabilityAiImageOptions;
import org.springframework.ai.zhipuai.ZhiPuAiImageOptions;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@ -168,15 +166,6 @@ public class AiImageServiceImpl implements AiImageService {
.model(model.getModel()).n(1)
.height(draw.getHeight()).width(draw.getWidth())
.build();
} else if (ObjUtil.equal(model.getPlatform(), AiPlatformEnum.YI_YAN.getPlatform())) {
return QianFanImageOptions.builder()
.model(model.getModel()).N(1)
.height(draw.getHeight()).width(draw.getWidth())
.build();
} else if (ObjUtil.equal(model.getPlatform(), AiPlatformEnum.ZHI_PU.getPlatform())) {
return ZhiPuAiImageOptions.builder()
.model(model.getModel())
.build();
}
throw new IllegalArgumentException("不支持的 AI 平台:" + model.getPlatform());
}

View File

@ -166,7 +166,7 @@ public class AiKnowledgeSegmentServiceImpl implements AiKnowledgeSegmentService
segmentMapper.deleteByIds(convertList(segments, AiKnowledgeSegmentDO::getId));
// 3. 删除向量存储中的段落
VectorStore vectorStore = getVectorStoreById(segments.get(0).getKnowledgeId());
VectorStore vectorStore = getVectorStoreById(segments.getFirst().getKnowledgeId());
vectorStore.delete(convertList(segments, AiKnowledgeSegmentDO::getVectorId));
}
@ -299,7 +299,7 @@ public class AiKnowledgeSegmentServiceImpl implements AiKnowledgeSegmentService
// 2. Rerank 重排序
if (rerankModel != null) {
RerankResponse rerankResponse = rerankModel.call(new RerankRequest(reqBO.getContent(), documents,
DashScopeRerankOptions.builder().withTopN(topK).build()));
DashScopeRerankOptions.builder().topN(topK).build()));
documents = convertList(rerankResponse.getResults(),
documentWithScore -> documentWithScore.getScore() >= similarityThreshold
? documentWithScore.getOutput() : null);

View File

@ -198,4 +198,4 @@ public class AiModelServiceImpl implements AiModelService {
}
}
}
}

View File

@ -8,23 +8,24 @@ import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
import cn.iocoder.yudao.module.ai.enums.model.AiPlatformEnum;
import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatOptions;
import org.springaicommunity.moonshot.MoonshotChatOptions;
import org.springaicommunity.qianfan.QianFanChatOptions;
import cn.hutool.extra.spring.SpringUtil;
import org.springframework.ai.anthropic.AnthropicChatOptions;
import org.springframework.ai.azure.openai.AzureOpenAiChatOptions;
import org.springframework.ai.chat.messages.*;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.ai.deepseek.DeepSeekAssistantMessage;
import org.springframework.ai.deepseek.DeepSeekChatOptions;
import org.springframework.ai.minimax.MiniMaxChatOptions;
import org.springframework.ai.google.genai.GoogleGenAiChatOptions;
import org.springframework.ai.ollama.api.OllamaChatOptions;
import org.springframework.ai.openai.OpenAiChatOptions;
import org.springframework.ai.tool.ToolCallback;
import org.springframework.ai.zhipuai.ZhiPuAiChatOptions;
import org.springframework.core.env.Environment;
import java.util.*;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.API_CONFIG_PLACEHOLDER_NOT_RESOLVED;
/**
* Spring AI 工具类
*
@ -35,6 +36,34 @@ public class AiUtils {
public static final String TOOL_CONTEXT_LOGIN_USER = "LOGIN_USER";
public static final String TOOL_CONTEXT_TENANT_ID = "TENANT_ID";
/**
* 解析 DB 等动态配置里的 Spring 占位符,例如 ${OPENAI_API_KEY}
*
* @param value 待解析的配置值
* @return 解析后的配置值
*/
public static String resolveSpringPlaceholders(String value) {
if (StrUtil.isBlank(value) || !StrUtil.contains(value, "${")) {
return value;
}
try {
return SpringUtil.getBean(Environment.class).resolveRequiredPlaceholders(value);
} catch (IllegalArgumentException ex) {
throw exception(API_CONFIG_PLACEHOLDER_NOT_RESOLVED, value);
}
}
/**
* 校验 API Key避免集成测试使用默认占位值发起调用。
*
* @param apiKey API Key
*/
public static void validateApiKey(String apiKey) {
if (StrUtil.isBlank(apiKey) || "sk-xxxx".equals(apiKey)) {
throw new IllegalStateException("apiKey 不能为空");
}
}
/**
* 通义千问支持多模态的模型
*
@ -42,7 +71,8 @@ public class AiUtils {
* @see <a href="https://help.aliyun.com/zh/model-studio/error-code#error-url">必须开启 withMultiModel 参数</a>
*/
public static final Set<String> TONG_YI_MULTI_MODELS = SetUtils.asSet(
// qwen3.5 / 3.6 系列(统一多模态主干)
// qwen3.5 / 3.6 / 3.7 系列(统一多模态主干)
"qwen3.7-max", "qwen3.7-plus", "qwen3.7-flash",
"qwen3.6-plus", "qwen3.6-flash",
"qwen3.5-plus", "qwen3.5-flash",
// qwen-vl 视觉理解
@ -72,32 +102,29 @@ public class AiUtils {
.enableThinking(true) // TODO 芋艿:默认都开启 thinking 模式,后续可以让用户配置
.multiModel(TONG_YI_MULTI_MODELS.contains(model)) // 是否多模态模型
.toolCallbacks(toolCallbacks).toolContext(toolContext).build();
case YI_YAN:
return QianFanChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens).build();
case DEEP_SEEK:
case DOU_BAO: // 复用 DeepSeek 客户端
case HUN_YUAN: // 复用 DeepSeek 客户端
case SILICON_FLOW: // 复用 DeepSeek 客户端
case YI_YAN: // 复用 DeepSeek 客户端
case ZHI_PU: // 复用 DeepSeek 客户端
case XING_HUO: // 复用 DeepSeek 客户端
case MINI_MAX: // 复用 DeepSeek 客户端
case MOONSHOT: // 复用 DeepSeek 客户端
case BAI_CHUAN: // 复用 DeepSeek 客户端
case STEP_FUN: // 复用 DeepSeek 客户端
return DeepSeekChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens)
.toolCallbacks(toolCallbacks).toolContext(toolContext).build();
case ZHI_PU:
return ZhiPuAiChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens)
.toolCallbacks(toolCallbacks).toolContext(toolContext).build();
case MINI_MAX:
return MiniMaxChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens)
.toolCallbacks(toolCallbacks).toolContext(toolContext).build();
case MOONSHOT:
return MoonshotChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens)
.toolCallbacks(toolCallbacks).toolContext(toolContext).build();
case OPENAI:
case GEMINI: // 复用 OpenAI 客户端
case BAI_CHUAN: // 复用 OpenAI 客户端
case GROK: // 复用 OpenAI 客户端
return OpenAiChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens)
.toolCallbacks(toolCallbacks).toolContext(toolContext).build();
case GEMINI:
return GoogleGenAiChatOptions.builder().model(model).temperature(temperature).maxOutputTokens(maxTokens)
.toolCallbacks(toolCallbacks).toolContext(toolContext).build();
case AZURE_OPENAI:
return AzureOpenAiChatOptions.builder().deploymentName(model).temperature(temperature).maxTokens(maxTokens)
return OpenAiChatOptions.builder().model(model).deploymentName(model).azure(true)
.temperature(temperature).maxTokens(maxTokens)
.toolCallbacks(toolCallbacks).toolContext(toolContext).build();
case ANTHROPIC:
return AnthropicChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens)
@ -159,4 +186,4 @@ public class AiUtils {
return MapUtil.getStr(output.getMetadata(), "reasoningContent");
}
}
}

View File

@ -0,0 +1,29 @@
package org.springframework.ai.model.tool;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.ChatOptions;
import java.lang.reflect.Method;
/**
* TODO 芋艿spring-ai-alibaba 2.0.0-M1.1 仍依赖旧的 Spring AI ToolExecutionEligibilityPredicate
* 临时补齐;升级到兼容 Spring AI 2.0.0 的 spring-ai-alibaba 版本后,删除本包下的 shim。
*/
public class DefaultToolExecutionEligibilityPredicate implements ToolExecutionEligibilityPredicate {
@Override
public boolean test(ChatOptions promptOptions, ChatResponse chatResponse) {
return isInternalToolExecutionEnabled(promptOptions) && chatResponse != null && chatResponse.hasToolCalls();
}
private static boolean isInternalToolExecutionEnabled(ChatOptions promptOptions) {
try {
Method method = promptOptions.getClass().getMethod("getInternalToolExecutionEnabled");
Object result = method.invoke(promptOptions);
return result == null || Boolean.TRUE.equals(result);
} catch (ReflectiveOperationException | SecurityException ignored) {
return true;
}
}
}

View File

@ -0,0 +1,21 @@
package org.springframework.ai.model.tool;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.util.Assert;
import java.util.function.BiPredicate;
/**
* TODO 芋艿spring-ai-alibaba 2.0.0-M1.1 仍依赖旧的 Spring AI ToolExecutionEligibilityPredicate
* 临时补齐;升级到兼容 Spring AI 2.0.0 的 spring-ai-alibaba 版本后,删除本包下的 shim。
*/
public interface ToolExecutionEligibilityPredicate extends BiPredicate<ChatOptions, ChatResponse> {
default boolean isToolExecutionRequired(ChatOptions promptOptions, ChatResponse chatResponse) {
Assert.notNull(promptOptions, "promptOptions cannot be null");
Assert.notNull(chatResponse, "chatResponse cannot be null");
return test(promptOptions, chatResponse);
}
}

View File

@ -1,10 +1,10 @@
package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat;
import cn.hutool.system.SystemUtil;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springframework.ai.anthropic.AnthropicChatModel;
import org.springframework.ai.anthropic.AnthropicChatOptions;
import org.springframework.ai.anthropic.api.AnthropicApi;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.SystemMessage;
import org.springframework.ai.chat.messages.UserMessage;
@ -14,6 +14,9 @@ import reactor.core.publisher.Flux;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import static cn.iocoder.yudao.module.ai.util.AiUtils.validateApiKey;
/**
* {@link AnthropicChatModel} 集成测试类
@ -22,13 +25,18 @@ import java.util.List;
*/
public class AnthropicChatModelTest {
private static final String BASE_URL = SystemUtil.get("ANTHROPIC_BASE_URL",
"https://api.teamorouter.com");
private static final String API_KEY = SystemUtil.get("ANTHROPIC_API_KEY",
"sk-xxxx"); // 按需改成你的 Anthropic API Key
private static final String MODEL = SystemUtil.get("ANTHROPIC_MODEL",
"claude-sonnet-4-6");
private final AnthropicChatModel chatModel = AnthropicChatModel.builder()
.anthropicApi(AnthropicApi.builder()
.apiKey("sk-muubv7cXeLw0Etgs743f365cD5Ea44429946Fa7e672d8942")
.baseUrl("https://aihubmix.com")
.build())
.defaultOptions(AnthropicChatOptions.builder()
.model(AnthropicApi.ChatModel.CLAUDE_SONNET_4_5)
.options(AnthropicChatOptions.builder()
.baseUrl(BASE_URL)
.apiKey(API_KEY)
.model(MODEL)
.temperature(0.7)
.maxTokens(4096)
.build())
@ -37,6 +45,7 @@ public class AnthropicChatModelTest {
@Test
@Disabled
public void testCall() {
validateApiKey(API_KEY);
// 准备参数
List<Message> messages = new ArrayList<>();
messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
@ -46,11 +55,13 @@ public class AnthropicChatModelTest {
ChatResponse response = chatModel.call(new Prompt(messages));
// 打印结果
System.out.println(response);
System.out.println(Objects.requireNonNull(response.getResult()).getOutput());
}
@Test
@Disabled
public void testStream() {
validateApiKey(API_KEY);
// 准备参数
List<Message> messages = new ArrayList<>();
messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
@ -59,20 +70,25 @@ public class AnthropicChatModelTest {
// 调用
Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages));
// 打印结果
flux.doOnNext(System.out::println).then().block();
flux.doOnNext(response -> {
// System.out.println(response);
System.out.println(Objects.requireNonNull(response.getResult()).getOutput());
}).then().block();
}
// TODO @芋艿:需要等 spring ai 升级https://github.com/spring-projects/spring-ai/pull/2800
@Test
@Disabled
public void testStream_thinking() {
validateApiKey(API_KEY);
// 准备参数
List<Message> messages = new ArrayList<>();
messages.add(new UserMessage("thkinking 下1+1 为什么等于 2 "));
AnthropicChatOptions options = AnthropicChatOptions.builder()
.model(AnthropicApi.ChatModel.CLAUDE_SONNET_4_5)
.thinking(AnthropicApi.ThinkingType.ENABLED, 3096)
.temperature(1D)
.baseUrl(BASE_URL)
.apiKey(API_KEY)
.model(MODEL)
.thinkingEnabled(1024) // https://platform.claude.com/docs/en/build-with-claude/extended-thinking
.maxTokens(4096)
.build();
// 调用
@ -80,7 +96,7 @@ public class AnthropicChatModelTest {
// 打印结果
flux.doOnNext(response -> {
// System.out.println(response);
System.out.println(response.getResult());
System.out.println(Objects.requireNonNull(response.getResult()).getOutput());
}).then().block();
}

View File

@ -1,44 +1,52 @@
package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat;
import com.azure.ai.openai.OpenAIClientBuilder;
import com.azure.core.credential.AzureKeyCredential;
import cn.hutool.system.SystemUtil;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springframework.ai.azure.openai.AzureOpenAiChatModel;
import org.springframework.ai.azure.openai.AzureOpenAiChatOptions;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.SystemMessage;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.ai.openai.OpenAiChatOptions;
import reactor.core.publisher.Flux;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import static org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiChatProperties.DEFAULT_DEPLOYMENT_NAME;
import static cn.iocoder.yudao.module.ai.util.AiUtils.validateApiKey;
/**
* {@link AzureOpenAiChatModel} 集成测试
* Azure OpenAI 集成测试
*
* @author 芋道源码
*/
public class AzureOpenAIChatModelTests {
// TODO @芋艿:晚点在调整
private final OpenAIClientBuilder openAiApi = new OpenAIClientBuilder()
.endpoint("https://eastusprejade.openai.azure.com")
.credential(new AzureKeyCredential("xxx"));
private final AzureOpenAiChatModel chatModel = AzureOpenAiChatModel.builder()
.openAIClientBuilder(openAiApi)
.defaultOptions(AzureOpenAiChatOptions.builder()
.deploymentName(DEFAULT_DEPLOYMENT_NAME)
private static final String BASE_URL = SystemUtil.get("AZURE_OPENAI_BASE_URL",
"https://xxx.openai.azure.com");
private static final String API_KEY = SystemUtil.get("AZURE_OPENAI_API_KEY",
"sk-xxxx"); // 按需改成你的 Azure OpenAI API Key
private static final String DEPLOYMENT_NAME = SystemUtil.get("AZURE_OPENAI_DEPLOYMENT_NAME",
"gpt-5.4"); // Azure 上创建的模型部署名称
private final OpenAiChatModel chatModel = OpenAiChatModel.builder()
.options(OpenAiChatOptions.builder()
.baseUrl(BASE_URL)
.apiKey(API_KEY)
.model(DEPLOYMENT_NAME)
.microsoftFoundry(true)
.deploymentName(DEPLOYMENT_NAME)
.temperature(0.7)
.build())
.build();
@Test
@Disabled
public void testCall() {
validateApiKey(API_KEY);
// 准备参数
List<Message> messages = new ArrayList<>();
messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
@ -48,12 +56,13 @@ public class AzureOpenAIChatModelTests {
ChatResponse response = chatModel.call(new Prompt(messages));
// 打印结果
System.out.println(response);
System.out.println(response.getResult().getOutput());
System.out.println(Objects.requireNonNull(response.getResult()).getOutput());
}
@Test
@Disabled
public void testStream() {
validateApiKey(API_KEY);
// 准备参数
List<Message> messages = new ArrayList<>();
messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
@ -64,7 +73,7 @@ public class AzureOpenAIChatModelTests {
// 打印结果
flux.doOnNext(response -> {
// System.out.println(response);
System.out.println(response.getResult().getOutput());
System.out.println(Objects.requireNonNull(response.getResult()).getOutput());
}).then().block();
}

View File

@ -1,5 +1,6 @@
package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat;
import cn.hutool.system.SystemUtil;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.baichuan.BaiChuanChatModel;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
@ -8,13 +9,16 @@ import org.springframework.ai.chat.messages.SystemMessage;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.ai.openai.OpenAiChatOptions;
import org.springframework.ai.openai.api.OpenAiApi;
import org.springframework.ai.deepseek.DeepSeekChatModel;
import org.springframework.ai.deepseek.DeepSeekChatOptions;
import org.springframework.ai.deepseek.api.DeepSeekApi;
import reactor.core.publisher.Flux;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import static cn.iocoder.yudao.module.ai.util.AiUtils.validateApiKey;
/**
* {@link BaiChuanChatModel} 集成测试
@ -23,22 +27,27 @@ import java.util.List;
*/
public class BaiChuanChatModelTests {
private final OpenAiChatModel openAiChatModel = OpenAiChatModel.builder()
.openAiApi(OpenAiApi.builder()
private static final String API_KEY = SystemUtil.get("BAICHUAN_API_KEY",
"sk-xxxx"); // 按需改成你的百川 API Key
private static final String MODEL = SystemUtil.get("BAICHUAN_MODEL",
BaiChuanChatModel.MODEL_DEFAULT);
private final BaiChuanChatModel chatModel = new BaiChuanChatModel(DeepSeekChatModel.builder()
.deepSeekApi(DeepSeekApi.builder()
.baseUrl(BaiChuanChatModel.BASE_URL)
.apiKey("sk-61b6766a94c70786ed02673f5e16af3c") // apiKey
.completionsPath(BaiChuanChatModel.COMPLETE_PATH)
.apiKey(API_KEY)
.build())
.defaultOptions(OpenAiChatOptions.builder()
.model("Baichuan4-Turbo") // 模型https://platform.baichuan-ai.com/docs/api
.options(DeepSeekChatOptions.builder()
.model(MODEL)
.temperature(0.7)
.build())
.build();
private final BaiChuanChatModel chatModel = new BaiChuanChatModel(openAiChatModel);
.build());
@Test
@Disabled
public void testCall() {
validateApiKey(API_KEY);
// 准备参数
List<Message> messages = new ArrayList<>();
messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
@ -48,11 +57,13 @@ public class BaiChuanChatModelTests {
ChatResponse response = chatModel.call(new Prompt(messages));
// 打印结果
System.out.println(response);
System.out.println(Objects.requireNonNull(response.getResult()).getOutput());
}
@Test
@Disabled
public void testStream() {
validateApiKey(API_KEY);
// 准备参数
List<Message> messages = new ArrayList<>();
messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
@ -61,7 +72,10 @@ public class BaiChuanChatModelTests {
// 调用
Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages));
// 打印结果
flux.doOnNext(System.out::println).then().block();
flux.doOnNext(response -> {
// System.out.println(response);
System.out.println(Objects.requireNonNull(response.getResult()).getOutput());
}).then().block();
}
}

View File

@ -8,11 +8,12 @@ import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.ai.openai.api.OpenAiApi;
import org.springframework.ai.openai.OpenAiChatOptions;
import reactor.core.publisher.Flux;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
/**
* 基于 {@link OpenAiChatModel} 集成 Coze 测试
@ -22,7 +23,7 @@ import java.util.List;
public class CozeChatModelTests {
private final OpenAiChatModel chatModel = OpenAiChatModel.builder()
.openAiApi(OpenAiApi.builder()
.options(OpenAiChatOptions.builder()
.baseUrl("http://127.0.0.1:3000")
.apiKey("app-4hy2d7fJauSbrKbzTKX1afuP") // apiKey
.build())
@ -40,7 +41,7 @@ public class CozeChatModelTests {
ChatResponse response = chatModel.call(new Prompt(messages));
// 打印结果
System.out.println(response);
System.out.println(response.getResult().getOutput());
System.out.println(Objects.requireNonNull(response.getResult()).getOutput());
}
@Test
@ -56,7 +57,7 @@ public class CozeChatModelTests {
// 打印结果
flux.doOnNext(response -> {
// System.out.println(response);
System.out.println(response.getResult().getOutput());
System.out.println(Objects.requireNonNull(response.getResult()).getOutput());
}).then().block();
}

View File

@ -1,5 +1,6 @@
package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat;
import cn.hutool.system.SystemUtil;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springframework.ai.chat.messages.Message;
@ -14,6 +15,9 @@ import reactor.core.publisher.Flux;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import static cn.iocoder.yudao.module.ai.util.AiUtils.validateApiKey;
/**
* {@link DeepSeekChatModel} 集成测试
@ -22,12 +26,17 @@ import java.util.List;
*/
public class DeepSeekChatModelTests {
private static final String API_KEY = SystemUtil.get("DEEPSEEK_API_KEY",
"sk-xxxx");
private static final String MODEL = SystemUtil.get("DEEPSEEK_MODEL",
"deepseek-v4-flash");
private final DeepSeekChatModel chatModel = DeepSeekChatModel.builder()
.deepSeekApi(DeepSeekApi.builder()
.apiKey("sk-eaf4172a057344dd9bc64b1f806b6axx") // apiKey
.apiKey(API_KEY) // apiKey
.build())
.defaultOptions(DeepSeekChatOptions.builder()
.model("deepseek-chat") // 模型
.options(DeepSeekChatOptions.builder()
.model(MODEL) // 模型
.temperature(0.7)
.build())
.build();
@ -35,6 +44,7 @@ public class DeepSeekChatModelTests {
@Test
@Disabled
public void testCall() {
validateApiKey(API_KEY);
// 准备参数
List<Message> messages = new ArrayList<>();
messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
@ -43,12 +53,13 @@ public class DeepSeekChatModelTests {
// 调用
ChatResponse response = chatModel.call(new Prompt(messages));
// 打印结果
System.out.println(response);
System.out.println(Objects.requireNonNull(response.getResult()).getOutput());
}
@Test
@Disabled
public void testStream() {
validateApiKey(API_KEY);
// 准备参数
List<Message> messages = new ArrayList<>();
messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
@ -63,11 +74,12 @@ public class DeepSeekChatModelTests {
@Test
@Disabled
public void testStream_thinking() {
validateApiKey(API_KEY);
// 准备参数
List<Message> messages = new ArrayList<>();
messages.add(new UserMessage("详细分析下,如何设计一个电商系统?"));
DeepSeekChatOptions options = DeepSeekChatOptions.builder()
.model("deepseek-reasoner")
.model(MODEL)
.build();
// 调用

View File

@ -8,11 +8,12 @@ import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.ai.openai.api.OpenAiApi;
import org.springframework.ai.openai.OpenAiChatOptions;
import reactor.core.publisher.Flux;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
/**
* 基于 {@link OpenAiChatModel} 集成 Dify 测试
@ -22,7 +23,7 @@ import java.util.List;
public class DifyChatModelTests {
private final OpenAiChatModel chatModel = OpenAiChatModel.builder()
.openAiApi(OpenAiApi.builder()
.options(OpenAiChatOptions.builder()
.baseUrl("http://127.0.0.1:3000")
.apiKey("app-4hy2d7fJauSbrKbzTKX1afuP") // apiKey
.build())
@ -40,7 +41,7 @@ public class DifyChatModelTests {
ChatResponse response = chatModel.call(new Prompt(messages));
// 打印结果
System.out.println(response);
System.out.println(response.getResult().getOutput());
System.out.println(Objects.requireNonNull(response.getResult()).getOutput());
}
@Test
@ -56,7 +57,7 @@ public class DifyChatModelTests {
// 打印结果
flux.doOnNext(response -> {
// System.out.println(response);
System.out.println(response.getResult().getOutput());
System.out.println(Objects.requireNonNull(response.getResult()).getOutput());
}).then().block();
}

View File

@ -1,5 +1,6 @@
package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat;
import cn.hutool.system.SystemUtil;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.doubao.DouBaoChatModel;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
@ -15,6 +16,9 @@ import reactor.core.publisher.Flux;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import static cn.iocoder.yudao.module.ai.util.AiUtils.validateApiKey;
/**
* {@link DouBaoChatModel} 集成测试
@ -23,19 +27,19 @@ import java.util.List;
*/
public class DouBaoChatModelTests {
/**
* 相比 OpenAIChatModel 来说DeepSeekChatModel 可以兼容豆包的 thinking 能力!
*/
private static final String API_KEY = SystemUtil.get("DOUBAO_API_KEY",
"sk-xxxx"); // 按需改成你的豆包 API Key
private static final String MODEL = SystemUtil.get("DOUBAO_MODEL",
DouBaoChatModel.MODEL_DEFAULT);
private final DeepSeekChatModel openAiChatModel = DeepSeekChatModel.builder()
.deepSeekApi(DeepSeekApi.builder()
.baseUrl(DouBaoChatModel.BASE_URL)
.completionsPath(DouBaoChatModel.COMPLETE_PATH)
.apiKey("5c1b5747-26d2-4ebd-a4e0-dd0e8d8b4272") // apiKey
.apiKey(API_KEY)
.build())
.defaultOptions(DeepSeekChatOptions.builder()
.model("doubao-1-5-lite-32k-250115") // 模型doubao
// .model("doubao-seed-1-6-thinking-250715") // 模型doubao
// .model("deepseek-r1-250120") // 模型deepseek
.options(DeepSeekChatOptions.builder()
.model(MODEL)
.temperature(0.7)
.build())
.build();
@ -45,6 +49,7 @@ public class DouBaoChatModelTests {
@Test
@Disabled
public void testCall() {
validateApiKey(API_KEY);
// 准备参数
List<Message> messages = new ArrayList<>();
messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
@ -54,11 +59,13 @@ public class DouBaoChatModelTests {
ChatResponse response = chatModel.call(new Prompt(messages));
// 打印结果
System.out.println(response);
System.out.println(Objects.requireNonNull(response.getResult()).getOutput());
}
@Test
@Disabled
public void testStream() {
validateApiKey(API_KEY);
// 准备参数
List<Message> messages = new ArrayList<>();
messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
@ -67,25 +74,26 @@ public class DouBaoChatModelTests {
// 调用
Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages));
// 打印结果
flux.doOnNext(System.out::println).then().block();
flux.doOnNext(response -> {
// System.out.println(response);
System.out.println(Objects.requireNonNull(response.getResult()).getOutput());
}).then().block();
}
@Test
@Disabled
public void testStream_thinking() {
validateApiKey(API_KEY);
// 准备参数
List<Message> messages = new ArrayList<>();
messages.add(new UserMessage("详细分析下,如何设计一个电商系统?"));
DeepSeekChatOptions options = DeepSeekChatOptions.builder()
.model("doubao-seed-1-6-thinking-250715")
.build();
// 调用
Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages, options));
Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages));
// 打印结果
flux.doOnNext(response -> {
// System.out.println(response);
System.out.println(response.getResult().getOutput());
System.out.println(Objects.requireNonNull(response.getResult()).getOutput());
}).then().block();
}

View File

@ -8,11 +8,12 @@ import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.ai.openai.api.OpenAiApi;
import org.springframework.ai.openai.OpenAiChatOptions;
import reactor.core.publisher.Flux;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
/**
* 基于 {@link OpenAiChatModel} 集成 FastGPT 测试
@ -22,7 +23,7 @@ import java.util.List;
public class FastGPTChatModelTests {
private final OpenAiChatModel chatModel = OpenAiChatModel.builder()
.openAiApi(OpenAiApi.builder()
.options(OpenAiChatOptions.builder()
.baseUrl("https://cloud.fastgpt.cn/api")
.apiKey("fastgpt-aqcc61kFtF8CeaglnGAfQOCIDWwjGdJVJHv6hIlMo28otFlva2aZNK") // apiKey
.build())
@ -40,7 +41,7 @@ public class FastGPTChatModelTests {
ChatResponse response = chatModel.call(new Prompt(messages));
// 打印结果
System.out.println(response);
System.out.println(response.getResult().getOutput());
System.out.println(Objects.requireNonNull(response.getResult()).getOutput());
}
@Test
@ -56,7 +57,7 @@ public class FastGPTChatModelTests {
// 打印结果
flux.doOnNext(response -> {
// System.out.println(response);
System.out.println(response.getResult().getOutput());
System.out.println(Objects.requireNonNull(response.getResult()).getOutput());
}).then().block();
}

View File

@ -1,6 +1,9 @@
package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.gemini.GeminiChatModel;
import cn.hutool.core.util.StrUtil;
import cn.hutool.system.SystemUtil;
import com.google.genai.Client;
import com.google.genai.types.HttpOptions;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springframework.ai.chat.messages.Message;
@ -8,38 +11,48 @@ import org.springframework.ai.chat.messages.SystemMessage;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.ai.openai.OpenAiChatOptions;
import org.springframework.ai.openai.api.OpenAiApi;
import org.springframework.ai.google.genai.GoogleGenAiChatModel;
import org.springframework.ai.google.genai.GoogleGenAiChatOptions;
import reactor.core.publisher.Flux;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import static cn.iocoder.yudao.module.ai.util.AiUtils.validateApiKey;
/**
* {@link GeminiChatModel} 集成测试
* {@link GoogleGenAiChatModel} 集成测试
*
* @author 芋道源码
*/
public class GeminiChatModelTests {
private final OpenAiChatModel openAiChatModel = OpenAiChatModel.builder()
.openAiApi(OpenAiApi.builder()
.baseUrl(GeminiChatModel.BASE_URL)
.completionsPath(GeminiChatModel.COMPLETE_PATH)
.apiKey("AIzaSyAVoBxgoFvvte820vEQMma2LKBnC98bqMQ")
.build())
.defaultOptions(OpenAiChatOptions.builder()
.model(GeminiChatModel.MODEL_DEFAULT) // 模型
.temperature(0.7)
.build())
.build();
private static final String BASE_URL = SystemUtil.get("GEMINI_BASE_URL");
private static final String API_KEY = SystemUtil.get("GEMINI_API_KEY",
"sk-xxxx"); // 按需改成你的 Gemini API Key
private static final String MODEL = "gemini-3.5-flash";
private final GeminiChatModel chatModel = new GeminiChatModel(openAiChatModel);
private final GoogleGenAiChatModel chatModel = GoogleGenAiChatModel.builder()
.genAiClient(buildClient())
.options(GoogleGenAiChatOptions.builder()
.model(MODEL) // 模型
.temperature(0.7)
.build())
.build();
private static Client buildClient() {
Client.Builder builder = Client.builder().apiKey(API_KEY);
if (StrUtil.isNotBlank(BASE_URL)) {
builder.httpOptions(HttpOptions.builder().baseUrl(BASE_URL).build());
}
return builder.build();
}
@Test
@Disabled
public void testCall() {
validateApiKey(API_KEY);
// 准备参数
List<Message> messages = new ArrayList<>();
messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
@ -49,11 +62,13 @@ public class GeminiChatModelTests {
ChatResponse response = chatModel.call(new Prompt(messages));
// 打印结果
System.out.println(response);
System.out.println(Objects.requireNonNull(response.getResult()).getOutput());
}
@Test
@Disabled
public void testStream() {
validateApiKey(API_KEY);
// 准备参数
List<Message> messages = new ArrayList<>();
messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
@ -62,7 +77,32 @@ public class GeminiChatModelTests {
// 调用
Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages));
// 打印结果
flux.doOnNext(System.out::println).then().block();
flux.doOnNext(response -> {
System.out.println(response);
// System.out.println(Objects.requireNonNull(response.getResult()).getOutput());
}).then().block();
}
@Test
@Disabled
public void testStream_thinking() {
validateApiKey(API_KEY);
// 准备参数
List<Message> messages = new ArrayList<>();
messages.add(new UserMessage("详细分析下,如何设计一个电商系统?"));
GoogleGenAiChatOptions options = GoogleGenAiChatOptions.builder()
.model(MODEL)
.thinkingBudget(1024)
.includeThoughts(true)
.build();
// 调用
Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages, options));
// 打印结果
flux.doOnNext(response -> {
System.out.println(response);
// System.out.println(Objects.requireNonNull(response.getResult()).getOutput());
}).then().block();
}
}

View File

@ -0,0 +1,77 @@
package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat;
import cn.hutool.system.SystemUtil;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.grok.GrokChatModel;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.SystemMessage;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.ai.openai.OpenAiChatOptions;
import reactor.core.publisher.Flux;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import static cn.iocoder.yudao.module.ai.util.AiUtils.validateApiKey;
/**
* {@link GrokChatModel} 集成测试
*
* @author 芋道源码
*/
public class GrokChatModelTests {
private static final String API_KEY = SystemUtil.get("GROK_API_KEY",
"sk-xxxx");
private static final String MODEL = SystemUtil.get("GROK_MODEL",
GrokChatModel.MODEL_DEFAULT);
private final GrokChatModel chatModel = new GrokChatModel(OpenAiChatModel.builder()
.options(OpenAiChatOptions.builder()
.baseUrl(GrokChatModel.BASE_URL)
.apiKey(API_KEY)
.model(MODEL)
.temperature(0.7)
.build())
.build());
@Test
@Disabled
public void testCall() {
validateApiKey(API_KEY);
// 准备参数
List<Message> messages = new ArrayList<>();
messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
messages.add(new UserMessage("1 + 1 = "));
// 调用
ChatResponse response = chatModel.call(new Prompt(messages));
// 打印结果
System.out.println(response);
System.out.println(Objects.requireNonNull(response.getResult()).getOutput());
}
@Test
@Disabled
public void testStream() {
validateApiKey(API_KEY);
// 准备参数
List<Message> messages = new ArrayList<>();
messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
messages.add(new UserMessage("1 + 1 = "));
// 调用
Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages));
// 打印结果
flux.doOnNext(response -> {
// System.out.println(response);
System.out.println(Objects.requireNonNull(response.getResult()).getOutput());
}).then().block();
}
}

View File

@ -1,5 +1,6 @@
package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat;
import cn.hutool.system.SystemUtil;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.hunyuan.HunYuanChatModel;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
@ -15,6 +16,9 @@ import reactor.core.publisher.Flux;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import static cn.iocoder.yudao.module.ai.util.AiUtils.validateApiKey;
/**
* {@link HunYuanChatModel} 集成测试
@ -23,14 +27,19 @@ import java.util.List;
*/
public class HunYuanChatModelTests {
private static final String API_KEY = SystemUtil.get("HUNYUAN_API_KEY",
"sk-xxxx");
private static final String MODEL = SystemUtil.get("HUNYUAN_MODEL",
HunYuanChatModel.MODEL_DEFAULT);
private final DeepSeekChatModel openAiChatModel = DeepSeekChatModel.builder()
.deepSeekApi(DeepSeekApi.builder()
.baseUrl(HunYuanChatModel.BASE_URL)
.completionsPath(HunYuanChatModel.COMPLETE_PATH)
.apiKey("sk-abc") // apiKey
.apiKey(API_KEY) // apiKey
.build())
.defaultOptions(DeepSeekChatOptions.builder()
.model(HunYuanChatModel.MODEL_DEFAULT) // 模型
.options(DeepSeekChatOptions.builder()
.model(MODEL) // 模型
.temperature(0.7)
.build())
.build();
@ -40,6 +49,7 @@ public class HunYuanChatModelTests {
@Test
@Disabled
public void testCall() {
validateApiKey(API_KEY);
// 准备参数
List<Message> messages = new ArrayList<>();
messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
@ -48,12 +58,13 @@ public class HunYuanChatModelTests {
// 调用
ChatResponse response = chatModel.call(new Prompt(messages));
// 打印结果
System.out.println(response);
System.out.println(Objects.requireNonNull(response.getResult()).getOutput());
}
@Test
@Disabled
public void testStream() {
validateApiKey(API_KEY);
// 准备参数
List<Message> messages = new ArrayList<>();
messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
@ -68,12 +79,12 @@ public class HunYuanChatModelTests {
@Test
@Disabled
public void testStream_thinking() {
validateApiKey(API_KEY);
// 准备参数
List<Message> messages = new ArrayList<>();
messages.add(new UserMessage("详细分析下,如何设计一个电商系统?"));
DeepSeekChatOptions options = DeepSeekChatOptions.builder()
.model("hunyuan-a13b")
// .model("hunyuan-turbos-latest")
.model(MODEL)
.build();
// 调用
@ -85,66 +96,4 @@ public class HunYuanChatModelTests {
}).then().block();
}
private final DeepSeekChatModel deepSeekOpenAiChatModel = DeepSeekChatModel.builder()
.deepSeekApi(DeepSeekApi.builder()
.baseUrl(HunYuanChatModel.DEEP_SEEK_BASE_URL)
.completionsPath(HunYuanChatModel.COMPLETE_PATH)
.apiKey("sk-abc") // apiKey
.build())
.defaultOptions(DeepSeekChatOptions.builder()
// .model(HunYuanChatModel.DEEP_SEEK_MODEL_DEFAULT) // 模型("deepseek-v3"
.model("deepseek-r1") // 模型("deepseek-r1"
.temperature(0.7)
.build())
.build();
private final HunYuanChatModel deepSeekChatModel = new HunYuanChatModel(deepSeekOpenAiChatModel);
@Test
@Disabled
public void testCall_deepseek() {
// 准备参数
List<Message> messages = new ArrayList<>();
messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
messages.add(new UserMessage("1 + 1 = "));
// 调用
ChatResponse response = deepSeekChatModel.call(new Prompt(messages));
// 打印结果
System.out.println(response);
}
@Test
@Disabled
public void testStream_deepseek() {
// 准备参数
List<Message> messages = new ArrayList<>();
messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
messages.add(new UserMessage("1 + 1 = "));
// 调用
Flux<ChatResponse> flux = deepSeekChatModel.stream(new Prompt(messages));
// 打印结果
flux.doOnNext(System.out::println).then().block();
}
@Test
@Disabled
public void testStream_deepseek_thinking() {
// 准备参数
List<Message> messages = new ArrayList<>();
messages.add(new UserMessage("详细分析下,如何设计一个电商系统?"));
DeepSeekChatOptions options = DeepSeekChatOptions.builder()
.model("deepseek-r1")
.build();
// 调用
Flux<ChatResponse> flux = deepSeekChatModel.stream(new Prompt(messages, options));
// 打印结果
flux.doOnNext(response -> {
// System.out.println(response);
System.out.println(response.getResult().getOutput());
}).then().block();
}
}

View File

@ -27,7 +27,7 @@ public class LlamaChatModelTests {
.ollamaApi(OllamaApi.builder()
.baseUrl("http://127.0.0.1:11434") // Ollama 服务地址
.build())
.defaultOptions(OllamaChatOptions.builder()
.options(OllamaChatOptions.builder()
.model(OllamaModel.LLAMA3.getName()) // 模型
.build())
.build();

View File

@ -1,5 +1,7 @@
package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat;
import cn.hutool.system.SystemUtil;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.minimax.MiniMaxChatModel;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springframework.ai.chat.messages.Message;
@ -7,13 +9,16 @@ import org.springframework.ai.chat.messages.SystemMessage;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.minimax.MiniMaxChatModel;
import org.springframework.ai.minimax.MiniMaxChatOptions;
import org.springframework.ai.minimax.api.MiniMaxApi;
import org.springframework.ai.deepseek.DeepSeekChatModel;
import org.springframework.ai.deepseek.DeepSeekChatOptions;
import org.springframework.ai.deepseek.api.DeepSeekApi;
import reactor.core.publisher.Flux;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import static cn.iocoder.yudao.module.ai.util.AiUtils.validateApiKey;
/**
* {@link MiniMaxChatModel} 的集成测试
@ -22,14 +27,24 @@ import java.util.List;
*/
public class MiniMaxChatModelTests {
private final MiniMaxChatModel chatModel = new MiniMaxChatModel(
new MiniMaxApi("eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJHcm91cE5hbWUiOiLnjovmlofmlowiLCJVc2VyTmFtZSI6IueOi-aWh-aWjCIsIkFjY291bnQiOiIiLCJTdWJqZWN0SUQiOiIxODk3Mjg3MjQ5NDU2ODA4MzQ2IiwiUGhvbmUiOiIxNTYwMTY5MTM5OSIsIkdyb3VwSUQiOiIxODk3Mjg3MjQ5NDQ4NDE5NzM4IiwiUGFnZU5hbWUiOiIiLCJNYWlsIjoiIiwiQ3JlYXRlVGltZSI6IjIwMjUtMDMtMTEgMTI6NTI6MDIiLCJUb2tlblR5cGUiOjEsImlzcyI6Im1pbmltYXgifQ.aAuB7gWW_oA4IYhh-CF7c9MfWWxKN49B_HK-DYjXaDwwffhiG-H1571z1WQhp9QytWG-DqgLejneeSxkiq1wQIe3FsEP2wz4BmGBct31LehbJu8ehLxg_vg75Uod1nFAHbm5mZz6JSVLNIlSo87Xr3UtSzJhAXlapEkcqlA4YOzOpKrZ8l5_OJPTORTCmHWZYgJcRS-faNiH62ZnUEHUozesTFhubJHo5GfJCw_edlnmfSUocERV1BjWvenhZ9My-aYXNktcW9WaSj9l6gayV7A0Ium_PL55T9ln1PcI8gayiVUKJGJDoqNyF1AF9_aF9NOKtTnQzwNqnZdlTYH6hw"), // 密钥
MiniMaxChatOptions.builder()
.model(MiniMaxApi.ChatModel.ABAB_6_5_G_Chat.getValue()) // 模型
.build());
private static final String API_KEY = SystemUtil.get("MINIMAX_API_KEY",
"sk-xxxx"); // 按需改成你的 MiniMax API Key
private static final String MODEL = SystemUtil.get("MINIMAX_MODEL",
MiniMaxChatModel.MODEL_DEFAULT);
private final MiniMaxChatModel chatModel = new MiniMaxChatModel(DeepSeekChatModel.builder()
.deepSeekApi(DeepSeekApi.builder()
.baseUrl(MiniMaxChatModel.BASE_URL)
.apiKey(API_KEY)
.build())
.options(DeepSeekChatOptions.builder()
.model(MODEL)
.build())
.build());
@Test
@Disabled
public void testCall() {
validateApiKey(API_KEY);
// 准备参数
List<Message> messages = new ArrayList<>();
messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
@ -39,12 +54,13 @@ public class MiniMaxChatModelTests {
ChatResponse response = chatModel.call(new Prompt(messages));
// 打印结果
System.out.println(response);
System.out.println(response.getResult().getOutput());
System.out.println(Objects.requireNonNull(response.getResult()).getOutput());
}
@Test
@Disabled
public void testStream() {
validateApiKey(API_KEY);
// 准备参数
List<Message> messages = new ArrayList<>();
messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
@ -55,19 +71,19 @@ public class MiniMaxChatModelTests {
// 打印结果
flux.doOnNext(response -> {
// System.out.println(response);
System.out.println(response.getResult().getOutput());
System.out.println(Objects.requireNonNull(response.getResult()).getOutput());
}).then().block();
}
// TODO @芋艿:暂时没解析 reasoning_content 结果,需要等官方修复
@Test
@Disabled
public void testStream_thinking() {
validateApiKey(API_KEY);
// 准备参数
List<Message> messages = new ArrayList<>();
messages.add(new UserMessage("详细分析下,如何设计一个电商系统?"));
MiniMaxChatOptions options = MiniMaxChatOptions.builder()
.model("MiniMax-M1")
DeepSeekChatOptions options = DeepSeekChatOptions.builder()
.model(MODEL)
.build();
// 调用
@ -75,7 +91,7 @@ public class MiniMaxChatModelTests {
// 打印结果
flux.doOnNext(response -> {
// System.out.println(response);
System.out.println(response.getResult().getOutput());
System.out.println(Objects.requireNonNull(response.getResult()).getOutput());
}).then().block();
}

View File

@ -1,19 +1,24 @@
package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat;
import cn.hutool.system.SystemUtil;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.moonshot.MoonshotChatModel;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springaicommunity.moonshot.MoonshotChatModel;
import org.springaicommunity.moonshot.MoonshotChatOptions;
import org.springaicommunity.moonshot.api.MoonshotApi;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.SystemMessage;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.deepseek.DeepSeekChatModel;
import org.springframework.ai.deepseek.DeepSeekChatOptions;
import org.springframework.ai.deepseek.api.DeepSeekApi;
import reactor.core.publisher.Flux;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import static cn.iocoder.yudao.module.ai.util.AiUtils.validateApiKey;
/**
* {@link MoonshotChatModel} 的集成测试
@ -22,18 +27,27 @@ import java.util.List;
*/
public class MoonshotChatModelTests {
private final MoonshotChatModel chatModel = MoonshotChatModel.builder()
.moonshotApi(MoonshotApi.builder()
.apiKey("sk-aHYYV1SARscItye5QQRRNbXij4fy65Ee7pNZlC9gsSQnUKXA") // 密钥
private static final String API_KEY = SystemUtil.get("MOONSHOT_API_KEY",
"sk-xxxx"); // 按需改成你的 Moonshot API Key
private static final String MODEL = SystemUtil.get("MOONSHOT_MODEL",
MoonshotChatModel.MODEL_DEFAULT);
private final MoonshotChatModel chatModel = new MoonshotChatModel(DeepSeekChatModel.builder()
.deepSeekApi(DeepSeekApi.builder()
.baseUrl(MoonshotChatModel.BASE_URL)
.completionsPath(MoonshotChatModel.COMPLETE_PATH)
.apiKey(API_KEY)
.build())
.defaultOptions(MoonshotChatOptions.builder()
.model("kimi-k2-0711-preview") // 模型
.options(DeepSeekChatOptions.builder()
.model(MODEL)
.temperature(1D)
.build())
.build();
.build());
@Test
@Disabled
public void testCall() {
validateApiKey(API_KEY);
// 准备参数
List<Message> messages = new ArrayList<>();
messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
@ -43,12 +57,13 @@ public class MoonshotChatModelTests {
ChatResponse response = chatModel.call(new Prompt(messages));
// 打印结果
System.out.println(response);
System.out.println(response.getResult().getOutput());
System.out.println(Objects.requireNonNull(response.getResult()).getOutput());
}
@Test
@Disabled
public void testStream() {
validateApiKey(API_KEY);
// 准备参数
List<Message> messages = new ArrayList<>();
messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
@ -59,20 +74,19 @@ public class MoonshotChatModelTests {
// 打印结果
flux.doOnNext(response -> {
// System.out.println(response);
System.out.println(response.getResult().getOutput());
System.out.println(Objects.requireNonNull(response.getResult()).getOutput());
}).then().block();
}
// TODO @芋艿:暂时没解析 reasoning_content 结果,需要等官方修复
@Test
@Disabled
public void testStream_thinking() {
validateApiKey(API_KEY);
// 准备参数
List<Message> messages = new ArrayList<>();
messages.add(new UserMessage("详细分析下,如何设计一个电商系统?"));
MoonshotChatOptions options = MoonshotChatOptions.builder()
// .model("kimi-k2-0711-preview")
.model("kimi-thinking-preview")
DeepSeekChatOptions options = DeepSeekChatOptions.builder()
.model(MODEL)
.build();
// 调用
@ -80,7 +94,7 @@ public class MoonshotChatModelTests {
// 打印结果
flux.doOnNext(response -> {
// System.out.println(response);
System.out.println(response.getResult().getOutput());
System.out.println(Objects.requireNonNull(response.getResult()).getOutput());
}).then().block();
}

View File

@ -26,7 +26,7 @@ public class OllamaChatModelTests {
.ollamaApi(OllamaApi.builder()
.baseUrl("http://127.0.0.1:11434") // Ollama 服务地址
.build())
.defaultOptions(OllamaChatOptions.builder()
.options(OllamaChatOptions.builder()
// .model("qwen") // 模型https://ollama.com/library/qwen
.model("deepseek-r1") // 模型https://ollama.com/library/deepseek-r1
.build())

View File

@ -1,6 +1,6 @@
package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat;
import com.azure.ai.openai.models.ReasoningEffortValue;
import cn.hutool.system.SystemUtil;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springframework.ai.chat.messages.Message;
@ -10,11 +10,13 @@ import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.ai.openai.OpenAiChatOptions;
import org.springframework.ai.openai.api.OpenAiApi;
import reactor.core.publisher.Flux;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import static cn.iocoder.yudao.module.ai.util.AiUtils.validateApiKey;
/**
* {@link OpenAiChatModel} 集成测试
@ -23,14 +25,18 @@ import java.util.List;
*/
public class OpenAIChatModelTests {
private static final String BASE_URL = SystemUtil.get("OPENAI_BASE_URL",
"https://api.teamorouter.com/v1");
private static final String API_KEY = SystemUtil.get("OPENAI_API_KEY",
"sk-xxxx"); // 按需改成你的 OpenAI API Key
private static final String MODEL = SystemUtil.get("OPENAI_MODEL",
"gpt-5.5");
private final OpenAiChatModel chatModel = OpenAiChatModel.builder()
.openAiApi(OpenAiApi.builder()
.baseUrl("https://api.holdai.top")
.apiKey("sk-z5joyRoV1iFEnh2SAi8QPNrIZTXyQSyxTmD5CoNDQbFixK2l") // apiKey
.build())
.defaultOptions(OpenAiChatOptions.builder()
.model("gpt-5-nano-2025-08-07") // 模型
// .model(OpenAiApi.ChatModel.O1) // 模型
.options(OpenAiChatOptions.builder()
.baseUrl(BASE_URL)
.apiKey(API_KEY)
.model(MODEL)
.temperature(0.7)
.build())
.build();
@ -38,6 +44,7 @@ public class OpenAIChatModelTests {
@Test
@Disabled
public void testCall() {
validateApiKey(API_KEY);
// 准备参数
List<Message> messages = new ArrayList<>();
messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
@ -47,12 +54,13 @@ public class OpenAIChatModelTests {
ChatResponse response = chatModel.call(new Prompt(messages));
// 打印结果
System.out.println(response);
System.out.println(response.getResult().getOutput());
System.out.println(Objects.requireNonNull(response.getResult()).getOutput());
}
@Test
@Disabled
public void testStream() {
validateApiKey(API_KEY);
// 准备参数
List<Message> messages = new ArrayList<>();
messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
@ -63,7 +71,7 @@ public class OpenAIChatModelTests {
// 打印结果
flux.doOnNext(response -> {
// System.out.println(response);
System.out.println(response.getResult().getOutput());
System.out.println(Objects.requireNonNull(response.getResult()).getOutput());
}).then().block();
}
@ -71,14 +79,16 @@ public class OpenAIChatModelTests {
@Test
@Disabled
public void testStream_thinking() {
validateApiKey(API_KEY);
// 准备参数
List<Message> messages = new ArrayList<>();
messages.add(new UserMessage("详细分析下,如何设计一个电商系统?"));
OpenAiChatOptions options = OpenAiChatOptions.builder()
.model("gpt-5")
// .model(OpenAiApi.ChatModel.O4_MINI)
// .model("o3-pro")
.reasoningEffort(ReasoningEffortValue.LOW.getValue())
.baseUrl(BASE_URL)
.apiKey(API_KEY)
.model(MODEL)
.reasoningEffort("low") // https://help.openai.com/zh-hant/articles/5072518-controlling-the-length-of-openai-model-responses
.maxCompletionTokens(1024)
.build();
// 调用
@ -86,10 +96,8 @@ public class OpenAIChatModelTests {
// 打印结果
flux.doOnNext(response -> {
// System.out.println(response);
System.out.println(response.getResult().getOutput());
System.out.println(Objects.requireNonNull(response.getResult()).getOutput());
}).then().block();
}
}

View File

@ -1,5 +1,6 @@
package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat;
import cn.hutool.system.SystemUtil;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.siliconflow.SiliconFlowApiConstants;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.siliconflow.SiliconFlowChatModel;
import org.junit.jupiter.api.Disabled;
@ -16,6 +17,9 @@ import reactor.core.publisher.Flux;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import static cn.iocoder.yudao.module.ai.util.AiUtils.validateApiKey;
/**
* {@link SiliconFlowChatModel} 集成测试
@ -24,15 +28,22 @@ import java.util.List;
*/
public class SiliconFlowChatModelTests {
private static final String API_KEY = SystemUtil.get("SILICONFLOW_API_KEY",
"sk-xxxx"); // 按需改成你的 SiliconFlow API Key
private static final String MODEL = SystemUtil.get("SILICONFLOW_MODEL",
SiliconFlowApiConstants.MODEL_DEFAULT);
private final DeepSeekChatModel openAiChatModel = DeepSeekChatModel.builder()
.deepSeekApi(DeepSeekApi.builder()
.baseUrl(SiliconFlowApiConstants.DEFAULT_BASE_URL)
.apiKey("sk-epsakfenqnyzoxhmbucsxlhkdqlcbnimslqoivkshalvdozz") // apiKey
.apiKey(API_KEY)
.build())
.defaultOptions(DeepSeekChatOptions.builder()
.model(SiliconFlowApiConstants.MODEL_DEFAULT) // 模型
// .model("deepseek-ai/DeepSeek-R1") // 模型deepseek-ai/DeepSeek-R1可用赠费
// .model("Pro/deepseek-ai/DeepSeek-R1") // 模型Pro/deepseek-ai/DeepSeek-R1需要付费
.options(DeepSeekChatOptions.builder()
.model(MODEL) // 模型
// .model("deepseek-ai/DeepSeek-V4-Flash") // 模型
// .model("moonshotai/Kimi-K2.7-Code") // 模型
// .model("moonshotai/Kimi-K2.6") // 模型
// .model("zai-org/GLM-5.2") // 模型
.temperature(0.7)
.build())
.build();
@ -42,6 +53,7 @@ public class SiliconFlowChatModelTests {
@Test
@Disabled
public void testCall() {
validateApiKey(API_KEY);
// 准备参数
List<Message> messages = new ArrayList<>();
messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
@ -56,6 +68,7 @@ public class SiliconFlowChatModelTests {
@Test
@Disabled
public void testStream() {
validateApiKey(API_KEY);
// 准备参数
List<Message> messages = new ArrayList<>();
messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
@ -70,11 +83,12 @@ public class SiliconFlowChatModelTests {
@Test
@Disabled
public void testStream_thinking() {
validateApiKey(API_KEY);
// 准备参数
List<Message> messages = new ArrayList<>();
messages.add(new UserMessage("详细分析下,如何设计一个电商系统?"));
DeepSeekChatOptions options = DeepSeekChatOptions.builder()
.model("deepseek-ai/DeepSeek-R1")
.model(MODEL)
.build();
// 调用
@ -82,7 +96,7 @@ public class SiliconFlowChatModelTests {
// 打印结果
flux.doOnNext(response -> {
// System.out.println(response);
System.out.println(response.getResult().getOutput());
System.out.println(Objects.requireNonNull(response.getResult()).getOutput());
}).then().block();
}

View File

@ -0,0 +1,82 @@
package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat;
import cn.hutool.system.SystemUtil;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.stepfun.StepFunChatModel;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.SystemMessage;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.deepseek.DeepSeekChatModel;
import org.springframework.ai.deepseek.DeepSeekChatOptions;
import org.springframework.ai.deepseek.api.DeepSeekApi;
import reactor.core.publisher.Flux;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import static cn.iocoder.yudao.module.ai.util.AiUtils.validateApiKey;
/**
* {@link StepFunChatModel} 集成测试
*
* @author 芋道源码
*/
public class StepFunChatModelTests {
private static final String API_KEY = SystemUtil.get("STEPFUN_API_KEY",
"sk-xxxx");
private static final String MODEL = SystemUtil.get("STEPFUN_MODEL",
StepFunChatModel.MODEL_DEFAULT);
private final StepFunChatModel chatModel = new StepFunChatModel(DeepSeekChatModel.builder()
.deepSeekApi(DeepSeekApi.builder()
.baseUrl(StepFunChatModel.BASE_URL)
.completionsPath(StepFunChatModel.COMPLETE_PATH)
.apiKey(API_KEY)
.build())
.options(DeepSeekChatOptions.builder()
.model(MODEL)
.temperature(0.7)
.build())
.build());
@Test
@Disabled
public void testCall() {
validateApiKey(API_KEY);
// 准备参数
List<Message> messages = new ArrayList<>();
messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
messages.add(new UserMessage("1 + 1 = "));
// 调用
ChatResponse response = chatModel.call(new Prompt(messages));
// 打印结果
System.out.println(response);
System.out.println(Objects.requireNonNull(response.getResult()).getOutput());
}
@Test
@Disabled
public void testStream() {
validateApiKey(API_KEY);
// 准备参数
List<Message> messages = new ArrayList<>();
messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
messages.add(new UserMessage("1 + 1 = "));
// 调用
Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages));
// 打印结果
flux.doOnNext(response -> {
// System.out.println(response);
// System.out.println(Objects.requireNonNull(response.getResult()).getOutput());
System.out.println(response.getResult() != null ? response.getResult().getOutput() : null);
}).then().block();
}
}

View File

@ -1,6 +1,8 @@
package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat;
import cn.hutool.system.SystemUtil;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.module.ai.util.AiUtils;
import com.alibaba.cloud.ai.dashscope.api.DashScopeApi;
import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel;
import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatOptions;
@ -22,7 +24,9 @@ import reactor.core.publisher.Flux;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import static cn.iocoder.yudao.module.ai.util.AiUtils.validateApiKey;
import static java.util.Arrays.asList;
/**
@ -32,13 +36,18 @@ import static java.util.Arrays.asList;
*/
public class TongYiChatModelTests {
private static final String API_KEY = SystemUtil.get("DASHSCOPE_API_KEY",
"sk-xxxx"); // 按需改成你的 DashScope API Key
private static final String MODEL = SystemUtil.get("DASHSCOPE_MODEL",
"qwen3.7-plus");
private final DashScopeChatModel chatModel = DashScopeChatModel.builder()
.dashScopeApi(DashScopeApi.builder()
.apiKey("sk-cd9f39e99ea54840bd1888282325f55a") // https://bailian.console.aliyun.com/cn-beijing/?tab=model#/api-key 获取密钥
.apiKey(API_KEY)
.build())
.defaultOptions(DashScopeChatOptions.builder()
.multiModel(true) // 注意:当使用 qwen3.6-plus 等多模态模型需要设置为 true可见 https://help.aliyun.com/zh/model-studio/error-code#error-url 链接
.model("qwen3.6-plus") // 模型
.multiModel(AiUtils.TONG_YI_MULTI_MODELS.contains(MODEL)) // 多模态模型需要设置为 true可见 https://help.aliyun.com/zh/model-studio/error-code#error-url
.model(MODEL) // 模型
// .model("deepseek-r1") // 模型deepseek-r1
// .model("deepseek-v3") // 模型deepseek-v3
// .model("deepseek-r1-distill-qwen-1.5b") // 模型deepseek-r1-distill-qwen-1.5b
@ -49,6 +58,7 @@ public class TongYiChatModelTests {
@Test
@Disabled
public void testCall() {
validateApiKey(API_KEY);
// 准备参数
List<Message> messages = new ArrayList<>();
messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
@ -58,12 +68,13 @@ public class TongYiChatModelTests {
ChatResponse response = chatModel.call(new Prompt(messages));
// 打印结果
System.out.println(response);
System.out.println(response.getResult().getOutput());
System.out.println(Objects.requireNonNull(response.getResult()).getOutput());
}
@Test
@Disabled
public void testStream() {
validateApiKey(API_KEY);
// 准备参数
List<Message> messages = new ArrayList<>();
// messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
@ -74,18 +85,19 @@ public class TongYiChatModelTests {
// 打印结果
flux.doOnNext(response -> {
// System.out.println(response);
System.out.println(response.getResult().getOutput());
System.out.println(Objects.requireNonNull(response.getResult()).getOutput());
}).then().block();
}
@Test
@Disabled
public void testStream_thinking() {
validateApiKey(API_KEY);
// 准备参数
List<Message> messages = new ArrayList<>();
messages.add(new UserMessage("详细分析下,如何设计一个电商系统?"));
DashScopeChatOptions options = DashScopeChatOptions.builder()
.model("qwen3.6-plus").multiModel(true)
.model(MODEL).multiModel(AiUtils.TONG_YI_MULTI_MODELS.contains(MODEL))
// .withModel("qwen-max-2025-01-25")
.enableThinking(true) // 必须设置,否则会报错
.build();
@ -95,17 +107,18 @@ public class TongYiChatModelTests {
// 打印结果
flux.doOnNext(response -> {
// System.out.println(response);
System.out.println(response.getResult().getOutput());
System.out.println(Objects.requireNonNull(response.getResult()).getOutput());
}).then().block();
}
@Test
@Disabled
public void testRerank() {
validateApiKey(API_KEY);
// 准备环境
RerankModel rerankModel = new DashScopeRerankModel(
DashScopeApi.builder()
.apiKey("sk-47aa124781be4bfb95244cc62f63f7d0")
.apiKey(API_KEY)
.build());
// 准备参数
String query = "spring";

View File

@ -1,5 +1,6 @@
package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat;
import cn.hutool.system.SystemUtil;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.xinghuo.XingHuoChatModel;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
@ -8,13 +9,14 @@ import org.springframework.ai.chat.messages.SystemMessage;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.deepseek.DeepSeekChatModel;
import org.springframework.ai.deepseek.DeepSeekChatOptions;
import org.springframework.ai.deepseek.api.DeepSeekApi;
import reactor.core.publisher.Flux;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import static cn.iocoder.yudao.module.ai.util.AiUtils.validateApiKey;
/**
* {@link XingHuoChatModel} 集成测试
@ -23,24 +25,23 @@ import java.util.List;
*/
public class XingHuoChatModelTests {
private final DeepSeekChatModel openAiChatModel = DeepSeekChatModel.builder()
.deepSeekApi(DeepSeekApi.builder()
.baseUrl(XingHuoChatModel.BASE_URL_V2)
.completionsPath(XingHuoChatModel.BASE_COMPLETIONS_PATH_V2)
.apiKey("75b161ed2aef4719b275d6e7f2a4d4cd:YWYxYWI2MTA4ODI2NGZlYTQyNjAzZTcz") // appKey:secretKey
.build())
.defaultOptions(DeepSeekChatOptions.builder()
// .model("generalv3.5") // 模型
.model("x1") // 模型
.temperature(0.7)
.build())
.build();
private static final String API_KEY = SystemUtil.get("XINGHUO_API_KEY",
"sk-xxxx"); // 按需改成你的讯飞星火 API Key
private static final String MODEL = SystemUtil.get("XINGHUO_MODEL",
XingHuoChatModel.MODEL_DEFAULT);
private final XingHuoChatModel chatModel = new XingHuoChatModel(openAiChatModel);
private final XingHuoChatModel chatModel = XingHuoChatModel.builder()
.apiKey(API_KEY)
.options(DeepSeekChatOptions.builder()
.model(MODEL)
.temperature(0.7)
.build())
.build();
@Test
@Disabled
public void testCall() {
validateApiKey(API_KEY);
// 准备参数
List<Message> messages = new ArrayList<>();
messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
@ -55,6 +56,7 @@ public class XingHuoChatModelTests {
@Test
@Disabled
public void testStream() {
validateApiKey(API_KEY);
// 准备参数
List<Message> messages = new ArrayList<>();
messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
@ -69,11 +71,12 @@ public class XingHuoChatModelTests {
@Test
@Disabled
public void testStream_thinking() {
validateApiKey(API_KEY);
// 准备参数
List<Message> messages = new ArrayList<>();
messages.add(new UserMessage("详细分析下,如何设计一个电商系统?"));
DeepSeekChatOptions options = DeepSeekChatOptions.builder()
.model("x1")
.model(MODEL)
.build();
// 调用
@ -81,7 +84,7 @@ public class XingHuoChatModelTests {
// 打印结果
flux.doOnNext(response -> {
// System.out.println(response);
System.out.println(response.getResult().getOutput());
System.out.println(Objects.requireNonNull(response.getResult()).getOutput());
}).then().block();
}

View File

@ -1,62 +1,76 @@
package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat;
import cn.hutool.system.SystemUtil;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.yiyan.YiYanChatModel;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springaicommunity.qianfan.QianFanChatModel;
import org.springaicommunity.qianfan.QianFanChatOptions;
import org.springaicommunity.qianfan.api.QianFanApi;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.deepseek.DeepSeekChatModel;
import org.springframework.ai.deepseek.DeepSeekChatOptions;
import org.springframework.ai.deepseek.api.DeepSeekApi;
import reactor.core.publisher.Flux;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import static cn.iocoder.yudao.module.ai.util.AiUtils.validateApiKey;
// TODO @芋艿:百度千帆 API 提供了 V2 版本,目前 Spring AI 不兼容,可关键 <https://github.com/spring-projects/spring-ai/issues/2179> 进展
/**
* {@link QianFanChatModel} 的集成测试
* {@link YiYanChatModel} 的集成测试
*
* @author fansili
*/
public class YiYanChatModelTests {
private final QianFanChatModel chatModel = new QianFanChatModel(
new QianFanApi("DGnyzREuaY7av7c38bOM9Ji2", "9aR8myflEOPDrEeLhoXv0FdqANOAyIZW"), // 密钥
QianFanChatOptions.builder()
.model("ERNIE-4.5-8K-Preview")
.build()
);
private static final String API_KEY = SystemUtil.get("YIYAN_API_KEY",
"sk-xxxx"); // 按需改成你的文心一言 API Key
private static final String MODEL = SystemUtil.get("YIYAN_MODEL",
YiYanChatModel.MODEL_DEFAULT);
private final YiYanChatModel chatModel = new YiYanChatModel(DeepSeekChatModel.builder()
.deepSeekApi(DeepSeekApi.builder()
.baseUrl(YiYanChatModel.BASE_URL)
.apiKey(API_KEY)
.build())
.options(DeepSeekChatOptions.builder()
.model(MODEL)
.build())
.build());
@Test
@Disabled
public void testCall() {
validateApiKey(API_KEY);
// 准备参数
List<Message> messages = new ArrayList<>();
// TODO @芋艿:文心一言,只要带上 system message 就报错,已经各种测试,很莫名!
// messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
messages.add(new UserMessage("1 + 1 = "));
// 调用
ChatResponse response = chatModel.call(new Prompt(messages));
// 打印结果
System.out.println(response);
System.out.println(Objects.requireNonNull(response.getResult()).getOutput());
}
@Test
@Disabled
public void testStream() {
validateApiKey(API_KEY);
// 准备参数
List<Message> messages = new ArrayList<>();
// TODO @芋艿:文心一言,只要带上 system message 就报错,已经各种测试,很莫名!
// messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
messages.add(new UserMessage("1 + 1 = "));
// 调用
Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages));
// 打印结果
flux.doOnNext(System.out::println).then().block();
flux.doOnNext(response -> {
// System.out.println(response);
System.out.println(Objects.requireNonNull(response.getResult()).getOutput());
}).then().block();
}
}

View File

@ -1,5 +1,7 @@
package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat;
import cn.hutool.system.SystemUtil;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.zhipu.ZhiPuChatModel;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springframework.ai.chat.messages.Message;
@ -7,31 +9,43 @@ import org.springframework.ai.chat.messages.SystemMessage;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.zhipuai.ZhiPuAiChatModel;
import org.springframework.ai.zhipuai.ZhiPuAiChatOptions;
import org.springframework.ai.zhipuai.api.ZhiPuAiApi;
import org.springframework.ai.deepseek.DeepSeekChatModel;
import org.springframework.ai.deepseek.DeepSeekChatOptions;
import org.springframework.ai.deepseek.api.DeepSeekApi;
import reactor.core.publisher.Flux;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import static cn.iocoder.yudao.module.ai.util.AiUtils.validateApiKey;
/**
* {@link ZhiPuAiChatModel} 的集成测试
* {@link ZhiPuChatModel} 的集成测试
*
* @author 芋道源码
*/
public class ZhiPuAiChatModelTests {
private final ZhiPuAiChatModel chatModel = new ZhiPuAiChatModel(
ZhiPuAiApi.builder().apiKey("2f35fb6ca4ea41fab898729b7fac086c.6ESSfPcCkxaKEUlR").build(), // 密钥
ZhiPuAiChatOptions.builder()
.model(ZhiPuAiApi.ChatModel.GLM_4.getName()) // 模型
private static final String API_KEY = SystemUtil.get("ZHIPU_API_KEY",
"sk-xxxx"); // 按需改成你的智谱 API Key
private static final String MODEL = SystemUtil.get("ZHIPU_MODEL",
ZhiPuChatModel.MODEL_DEFAULT);
private final ZhiPuChatModel chatModel = new ZhiPuChatModel(DeepSeekChatModel.builder()
.deepSeekApi(DeepSeekApi.builder()
.baseUrl(ZhiPuChatModel.BASE_URL)
.apiKey(API_KEY)
.build())
.options(DeepSeekChatOptions.builder()
.model(MODEL)
.build()
);
).build());
@Test
@Disabled
public void testCall() {
validateApiKey(API_KEY);
// 准备参数
List<Message> messages = new ArrayList<>();
messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
@ -41,12 +55,13 @@ public class ZhiPuAiChatModelTests {
ChatResponse response = chatModel.call(new Prompt(messages));
// 打印结果
System.out.println(response);
System.out.println(response.getResult().getOutput());
System.out.println(Objects.requireNonNull(response.getResult()).getOutput());
}
@Test
@Disabled
public void testStream() {
validateApiKey(API_KEY);
// 准备参数
List<Message> messages = new ArrayList<>();
messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
@ -57,19 +72,19 @@ public class ZhiPuAiChatModelTests {
// 打印结果
flux.doOnNext(response -> {
// System.out.println(response);
System.out.println(response.getResult().getOutput());
System.out.println(Objects.requireNonNull(response.getResult()).getOutput());
}).then().block();
}
// TODO @芋艿:暂时没解析 reasoning_content 结果,需要等官方修复
@Test
@Disabled
public void testStream_thinking() {
validateApiKey(API_KEY);
// 准备参数
List<Message> messages = new ArrayList<>();
messages.add(new UserMessage("详细分析下,如何设计一个电商系统?"));
ZhiPuAiChatOptions options = ZhiPuAiChatOptions.builder()
.model("GLM-4.5")
DeepSeekChatOptions options = DeepSeekChatOptions.builder()
.model(MODEL)
.build();
// 调用
@ -77,7 +92,7 @@ public class ZhiPuAiChatModelTests {
// 打印结果
flux.doOnNext(response -> {
// System.out.println(response);
System.out.println(response.getResult().getOutput());
System.out.println(Objects.requireNonNull(response.getResult()).getOutput());
}).then().block();
}

View File

@ -7,7 +7,6 @@ import org.springframework.ai.image.ImagePrompt;
import org.springframework.ai.image.ImageResponse;
import org.springframework.ai.openai.OpenAiImageModel;
import org.springframework.ai.openai.OpenAiImageOptions;
import org.springframework.ai.openai.api.OpenAiImageApi;
/**
* {@link OpenAiImageModel} 集成测试类
@ -16,17 +15,19 @@ import org.springframework.ai.openai.api.OpenAiImageApi;
*/
public class OpenAiImageModelTests {
private final OpenAiImageModel imageModel = new OpenAiImageModel(OpenAiImageApi.builder()
.baseUrl("https://api.holdai.top") // apiKey
.apiKey("sk-PytRecQlmjEteoa2RRN6cGnwslo72UUPLQVNEMS6K9yjbmpD")
.build());
private final OpenAiImageModel imageModel = OpenAiImageModel.builder()
.options(OpenAiImageOptions.builder()
.baseUrl("https://api.holdai.top") // apiKey
.apiKey("sk-xxx")
.build())
.build();
@Test
@Disabled
public void testCall() {
// 准备参数
ImageOptions options = OpenAiImageOptions.builder()
.model(OpenAiImageApi.ImageModel.DALL_E_2.getValue()) // 这个模型比较便宜
.model("dall-e-2") // 这个模型比较便宜
.height(256).width(256)
.build();
ImagePrompt prompt = new ImagePrompt("中国长城!", options);

View File

@ -1,43 +0,0 @@
package cn.iocoder.yudao.module.ai.framework.ai.core.model.image;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springaicommunity.qianfan.QianFanImageModel;
import org.springaicommunity.qianfan.QianFanImageOptions;
import org.springaicommunity.qianfan.api.QianFanImageApi;
import org.springframework.ai.image.ImagePrompt;
import org.springframework.ai.image.ImageResponse;
import static cn.iocoder.yudao.module.ai.framework.ai.core.model.image.StabilityAiImageModelTests.viewImage;
// TODO @芋艿:百度千帆 API 提供了 V2 版本,目前 Spring AI 不兼容,可关键 <https://github.com/spring-projects/spring-ai/issues/2179> 进展
/**
* {@link QianFanImageModel} 集成测试类
*/
public class QianFanImageTests {
private final QianFanImageModel imageModel = new QianFanImageModel(
new QianFanImageApi("qS8k8dYr2nXunagK4SSU8Xjj", "pHGbx51ql2f0hOyabQvSZezahVC3hh3e")); // 密钥
@Test
@Disabled
public void testCall() {
// 准备参数
// 只支持 1024x1024、768x768、768x1024、1024x768、576x1024、1024x576
QianFanImageOptions imageOptions = QianFanImageOptions.builder()
.model(QianFanImageApi.ImageModel.Stable_Diffusion_XL.getValue())
.width(1024).height(1024)
.N(1)
.build();
ImagePrompt prompt = new ImagePrompt("good", imageOptions);
// 方法调用
ImageResponse response = imageModel.call(prompt);
// 打印结果
String b64Json = response.getResult().getOutput().getB64Json();
System.out.println(response);
viewImage(b64Json);
}
}

View File

@ -1,5 +1,6 @@
package cn.iocoder.yudao.module.ai.framework.ai.core.model.image;
import cn.hutool.system.SystemUtil;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.siliconflow.SiliconFlowImageApi;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.siliconflow.SiliconFlowImageModel;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.siliconflow.SiliconFlowImageOptions;
@ -8,18 +9,24 @@ import org.junit.jupiter.api.Test;
import org.springframework.ai.image.ImagePrompt;
import org.springframework.ai.image.ImageResponse;
import static cn.iocoder.yudao.module.ai.util.AiUtils.validateApiKey;
/**
* {@link SiliconFlowImageModel} 集成测试
*/
public class SiliconFlowImageModelTests {
private static final String API_KEY = SystemUtil.get("SILICONFLOW_API_KEY",
"sk-xxxx"); // 按需改成你的 SiliconFlow API Key
private final SiliconFlowImageModel imageModel = new SiliconFlowImageModel(
new SiliconFlowImageApi("sk-epsakfenqnyzoxhmbucsxlhkdqlcbnimslqoivkshalvdozz") // 密钥
new SiliconFlowImageApi(API_KEY)
);
@Test
@Disabled
public void testCall() {
validateApiKey(API_KEY);
// 准备参数
SiliconFlowImageOptions imageOptions = SiliconFlowImageOptions.builder()
.model("Kwai-Kolors/Kolors")

Some files were not shown because too many files have changed in this diff Show More