# Conflicts:
#	yudao-dependencies/pom.xml
#	yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/config/YudaoMybatisAutoConfiguration.java
#	yudao-gateway/src/main/resources/application.yaml
#	yudao-module-ai/yudao-module-ai-server/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatMessageServiceImpl.java
#	yudao-module-ai/yudao-module-ai-server/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentServiceImpl.java
#	yudao-module-ai/yudao-module-ai-server/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiToolServiceImpl.java
#	yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/AliyunSmsClient.java
This commit is contained in:
YunaiV 2025-08-29 20:22:25 +08:00
commit c015b68db8
87 changed files with 2482 additions and 313 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 32 KiB

View File

@ -33,11 +33,11 @@
<mybatis-plus-join.version>1.5.4</mybatis-plus-join.version> <mybatis-plus-join.version>1.5.4</mybatis-plus-join.version>
<dynamic-datasource.version>4.3.1</dynamic-datasource.version> <dynamic-datasource.version>4.3.1</dynamic-datasource.version>
<easy-trans.version>3.0.6</easy-trans.version> <easy-trans.version>3.0.6</easy-trans.version>
<redisson.version>3.50.0</redisson.version> <redisson.version>3.51.0</redisson.version>
<dm8.jdbc.version>8.1.3.140</dm8.jdbc.version> <dm8.jdbc.version>8.1.3.140</dm8.jdbc.version>
<kingbase.jdbc.version>8.6.0</kingbase.jdbc.version> <kingbase.jdbc.version>8.6.0</kingbase.jdbc.version>
<opengauss.jdbc.version>5.1.0</opengauss.jdbc.version> <opengauss.jdbc.version>5.1.0</opengauss.jdbc.version>
<taos.version>3.3.3</taos.version> <taos.version>3.7.3</taos.version>
<!-- 消息队列 --> <!-- 消息队列 -->
<rocketmq-spring.version>2.3.4</rocketmq-spring.version> <rocketmq-spring.version>2.3.4</rocketmq-spring.version>
<!-- RPC 相关 --> <!-- RPC 相关 -->
@ -58,11 +58,11 @@
<flowable.version>6.8.0</flowable.version> <flowable.version>6.8.0</flowable.version>
<!-- 工具类相关 --> <!-- 工具类相关 -->
<anji-plus-captcha.version>1.4.0</anji-plus-captcha.version> <anji-plus-captcha.version>1.4.0</anji-plus-captcha.version>
<jsoup.version>1.21.1</jsoup.version> <jsoup.version>1.21.2</jsoup.version>
<lombok.version>1.18.38</lombok.version> <lombok.version>1.18.38</lombok.version>
<mapstruct.version>1.6.3</mapstruct.version> <mapstruct.version>1.6.3</mapstruct.version>
<hutool.version>5.8.39</hutool.version> <hutool-5.version>5.8.40</hutool-5.version>
<fastexcel.version>1.2.0</fastexcel.version> <fastexcel.version>1.3.0</fastexcel.version>
<velocity.version>2.4</velocity.version> <!-- JDK8 不能从 2.4 升级到 2.4.1,会报包不存在!!!! --> <velocity.version>2.4</velocity.version> <!-- JDK8 不能从 2.4 升级到 2.4.1,会报包不存在!!!! -->
<fastjson.version>1.2.83</fastjson.version> <fastjson.version>1.2.83</fastjson.version>
<guava.version>33.4.8-jre</guava.version> <guava.version>33.4.8-jre</guava.version>
@ -84,7 +84,7 @@
<justauth-starter.version>1.4.0</justauth-starter.version> <justauth-starter.version>1.4.0</justauth-starter.version>
<jimureport.version>2.1.0</jimureport.version> <jimureport.version>2.1.0</jimureport.version>
<jimubi.version>1.9.5</jimubi.version> <jimubi.version>1.9.5</jimubi.version>
<weixin-java.version>4.7.5.B</weixin-java.version> <weixin-java.version>4.7.7-20250808.182223</weixin-java.version>
<!-- 专属于 JDK8 安全漏洞升级 --> <!-- 专属于 JDK8 安全漏洞升级 -->
<logback.version>1.2.13</logback.version> <!-- 无法使用 1.3.X 版本,启动会报错 --> <logback.version>1.2.13</logback.version> <!-- 无法使用 1.3.X 版本,启动会报错 -->
</properties> </properties>
@ -546,7 +546,7 @@
<dependency> <dependency>
<groupId>cn.hutool</groupId> <groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId> <artifactId>hutool-all</artifactId>
<version>${hutool.version}</version> <version>${hutool-5.version}</version>
</dependency> </dependency>
<dependency> <dependency>

View File

@ -69,17 +69,17 @@ public class LocalDateTimeUtils {
* 创建指定时间 * 创建指定时间
* *
* @param year * @param year
* @param mouth * @param month
* @param day * @param day
* @return 指定时间 * @return 指定时间
*/ */
public static LocalDateTime buildTime(int year, int mouth, int day) { public static LocalDateTime buildTime(int year, int month, int day) {
return LocalDateTime.of(year, mouth, day, 0, 0, 0); return LocalDateTime.of(year, month, day, 0, 0, 0);
} }
public static LocalDateTime[] buildBetweenTime(int year1, int mouth1, int day1, public static LocalDateTime[] buildBetweenTime(int year1, int month1, int day1,
int year2, int mouth2, int day2) { int year2, int month2, int day2) {
return new LocalDateTime[]{buildTime(year1, mouth1, day1), buildTime(year2, mouth2, day2)}; return new LocalDateTime[]{buildTime(year1, month1, day1), buildTime(year2, month2, day2)};
} }
/** /**

View File

@ -75,7 +75,7 @@ public class TenantDatabaseInterceptor implements TenantLineHandler {
if (TenantBaseDO.class.isAssignableFrom(tableInfo.getEntityType())) { if (TenantBaseDO.class.isAssignableFrom(tableInfo.getEntityType())) {
return false; return false;
} }
// 如果添加了 @TenantIgnore 注解显然也不忽略租户 // 如果添加了 @TenantIgnore 注解忽略租户
TenantIgnore tenantIgnore = tableInfo.getEntityType().getAnnotation(TenantIgnore.class); TenantIgnore tenantIgnore = tableInfo.getEntityType().getAnnotation(TenantIgnore.class);
return tenantIgnore != null; return tenantIgnore != null;
} }

View File

@ -12,6 +12,7 @@ import org.springframework.beans.factory.config.BeanPostProcessor;
public class TenantRabbitMQInitializer implements BeanPostProcessor { public class TenantRabbitMQInitializer implements BeanPostProcessor {
@Override @Override
@SuppressWarnings("PatternVariableCanBeUsed")
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof RabbitTemplate) { if (bean instanceof RabbitTemplate) {
RabbitTemplate rabbitTemplate = (RabbitTemplate) bean; RabbitTemplate rabbitTemplate = (RabbitTemplate) bean;
@ -20,4 +21,4 @@ public class TenantRabbitMQInitializer implements BeanPostProcessor {
return bean; return bean;
} }
} }

View File

@ -17,6 +17,7 @@ import org.springframework.beans.factory.config.BeanPostProcessor;
public class TenantRocketMQInitializer implements BeanPostProcessor { public class TenantRocketMQInitializer implements BeanPostProcessor {
@Override @Override
@SuppressWarnings("PatternVariableCanBeUsed")
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof DefaultRocketMQListenerContainer) { if (bean instanceof DefaultRocketMQListenerContainer) {
DefaultRocketMQListenerContainer container = (DefaultRocketMQListenerContainer) bean; DefaultRocketMQListenerContainer container = (DefaultRocketMQListenerContainer) bean;
@ -50,4 +51,4 @@ public class TenantRocketMQInitializer implements BeanPostProcessor {
consumerImpl.registerConsumeMessageHook(new TenantRocketMQConsumeMessageHook()); consumerImpl.registerConsumeMessageHook(new TenantRocketMQConsumeMessageHook());
} }
} }

View File

@ -21,6 +21,7 @@ public class YudaoAsyncAutoConfiguration {
return new BeanPostProcessor() { return new BeanPostProcessor() {
@Override @Override
@SuppressWarnings("PatternVariableCanBeUsed")
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
// 处理 ThreadPoolTaskExecutor // 处理 ThreadPoolTaskExecutor
if (bean instanceof ThreadPoolTaskExecutor) { if (bean instanceof ThreadPoolTaskExecutor) {

View File

@ -10,7 +10,6 @@ import com.baomidou.mybatisplus.extension.incrementer.*;
import com.baomidou.mybatisplus.extension.parser.JsqlParserGlobal; import com.baomidou.mybatisplus.extension.parser.JsqlParserGlobal;
import com.baomidou.mybatisplus.extension.parser.cache.JdkSerialCaffeineJsqlParseCache; import com.baomidou.mybatisplus.extension.parser.cache.JdkSerialCaffeineJsqlParseCache;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.BlockAttackInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
import org.mybatis.spring.annotation.MapperScan; import org.mybatis.spring.annotation.MapperScan;
@ -43,7 +42,8 @@ public class YudaoMybatisAutoConfiguration {
public MybatisPlusInterceptor mybatisPlusInterceptor() { public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor(); MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor()); // 分页插件 mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor()); // 分页插件
mybatisPlusInterceptor.addInnerInterceptor(new BlockAttackInnerInterceptor()); // 拦截没有指定条件的 update delete 语句 // 按需开启可能会影响到 updateBatch 的地方例如说文件配置管理
// mybatisPlusInterceptor.addInnerInterceptor(new BlockAttackInnerInterceptor()); // 拦截没有指定条件的 update delete 语句
return mybatisPlusInterceptor; return mybatisPlusInterceptor;
} }

View File

@ -2,7 +2,6 @@ package cn.iocoder.yudao.framework.mybatis.core.handler;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils; import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import org.apache.ibatis.reflection.MetaObject; import org.apache.ibatis.reflection.MetaObject;
@ -19,6 +18,7 @@ import java.util.Objects;
public class DefaultDBFieldHandler implements MetaObjectHandler { public class DefaultDBFieldHandler implements MetaObjectHandler {
@Override @Override
@SuppressWarnings("PatternVariableCanBeUsed")
public void insertFill(MetaObject metaObject) { public void insertFill(MetaObject metaObject) {
if (Objects.nonNull(metaObject) && metaObject.getOriginalObject() instanceof BaseDO) { if (Objects.nonNull(metaObject) && metaObject.getOriginalObject() instanceof BaseDO) {
BaseDO baseDO = (BaseDO) metaObject.getOriginalObject(); BaseDO baseDO = (BaseDO) metaObject.getOriginalObject();

View File

@ -1,7 +1,7 @@
package cn.iocoder.yudao.framework.idempotent.core.keyresolver.impl; package cn.iocoder.yudao.framework.idempotent.core.keyresolver.impl;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.SecureUtil; import cn.hutool.crypto.SecureUtil;
import cn.iocoder.yudao.framework.common.util.string.StrUtils;
import cn.iocoder.yudao.framework.idempotent.core.annotation.Idempotent; import cn.iocoder.yudao.framework.idempotent.core.annotation.Idempotent;
import cn.iocoder.yudao.framework.idempotent.core.keyresolver.IdempotentKeyResolver; import cn.iocoder.yudao.framework.idempotent.core.keyresolver.IdempotentKeyResolver;
import org.aspectj.lang.JoinPoint; import org.aspectj.lang.JoinPoint;
@ -18,7 +18,7 @@ public class DefaultIdempotentKeyResolver implements IdempotentKeyResolver {
@Override @Override
public String resolver(JoinPoint joinPoint, Idempotent idempotent) { public String resolver(JoinPoint joinPoint, Idempotent idempotent) {
String methodName = joinPoint.getSignature().toString(); String methodName = joinPoint.getSignature().toString();
String argsStr = StrUtil.join(",", joinPoint.getArgs()); String argsStr = StrUtils.joinMethodArgs(joinPoint);
return SecureUtil.md5(methodName + argsStr); return SecureUtil.md5(methodName + argsStr);
} }

View File

@ -1,7 +1,7 @@
package cn.iocoder.yudao.framework.idempotent.core.keyresolver.impl; package cn.iocoder.yudao.framework.idempotent.core.keyresolver.impl;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.SecureUtil; import cn.hutool.crypto.SecureUtil;
import cn.iocoder.yudao.framework.common.util.string.StrUtils;
import cn.iocoder.yudao.framework.idempotent.core.annotation.Idempotent; import cn.iocoder.yudao.framework.idempotent.core.annotation.Idempotent;
import cn.iocoder.yudao.framework.idempotent.core.keyresolver.IdempotentKeyResolver; import cn.iocoder.yudao.framework.idempotent.core.keyresolver.IdempotentKeyResolver;
import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils; import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils;
@ -19,7 +19,7 @@ public class UserIdempotentKeyResolver implements IdempotentKeyResolver {
@Override @Override
public String resolver(JoinPoint joinPoint, Idempotent idempotent) { public String resolver(JoinPoint joinPoint, Idempotent idempotent) {
String methodName = joinPoint.getSignature().toString(); String methodName = joinPoint.getSignature().toString();
String argsStr = StrUtil.join(",", joinPoint.getArgs()); String argsStr = StrUtils.joinMethodArgs(joinPoint);
Long userId = WebFrameworkUtils.getLoginUserId(); Long userId = WebFrameworkUtils.getLoginUserId();
Integer userType = WebFrameworkUtils.getLoginUserType(); Integer userType = WebFrameworkUtils.getLoginUserType();
return SecureUtil.md5(methodName + argsStr + userId + userType); return SecureUtil.md5(methodName + argsStr + userId + userType);

View File

@ -1,8 +1,8 @@
package cn.iocoder.yudao.framework.ratelimiter.core.keyresolver.impl; package cn.iocoder.yudao.framework.ratelimiter.core.keyresolver.impl;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.SecureUtil; import cn.hutool.crypto.SecureUtil;
import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils; import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;
import cn.iocoder.yudao.framework.common.util.string.StrUtils;
import cn.iocoder.yudao.framework.ratelimiter.core.annotation.RateLimiter; import cn.iocoder.yudao.framework.ratelimiter.core.annotation.RateLimiter;
import cn.iocoder.yudao.framework.ratelimiter.core.keyresolver.RateLimiterKeyResolver; import cn.iocoder.yudao.framework.ratelimiter.core.keyresolver.RateLimiterKeyResolver;
import org.aspectj.lang.JoinPoint; import org.aspectj.lang.JoinPoint;
@ -19,7 +19,7 @@ public class ClientIpRateLimiterKeyResolver implements RateLimiterKeyResolver {
@Override @Override
public String resolver(JoinPoint joinPoint, RateLimiter rateLimiter) { public String resolver(JoinPoint joinPoint, RateLimiter rateLimiter) {
String methodName = joinPoint.getSignature().toString(); String methodName = joinPoint.getSignature().toString();
String argsStr = StrUtil.join(",", joinPoint.getArgs()); String argsStr = StrUtils.joinMethodArgs(joinPoint);
String clientIp = ServletUtils.getClientIP(); String clientIp = ServletUtils.getClientIP();
return SecureUtil.md5(methodName + argsStr + clientIp); return SecureUtil.md5(methodName + argsStr + clientIp);
} }

View File

@ -1,7 +1,7 @@
package cn.iocoder.yudao.framework.ratelimiter.core.keyresolver.impl; package cn.iocoder.yudao.framework.ratelimiter.core.keyresolver.impl;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.SecureUtil; import cn.hutool.crypto.SecureUtil;
import cn.iocoder.yudao.framework.common.util.string.StrUtils;
import cn.iocoder.yudao.framework.ratelimiter.core.annotation.RateLimiter; import cn.iocoder.yudao.framework.ratelimiter.core.annotation.RateLimiter;
import cn.iocoder.yudao.framework.ratelimiter.core.keyresolver.RateLimiterKeyResolver; import cn.iocoder.yudao.framework.ratelimiter.core.keyresolver.RateLimiterKeyResolver;
import org.aspectj.lang.JoinPoint; import org.aspectj.lang.JoinPoint;
@ -18,7 +18,7 @@ public class DefaultRateLimiterKeyResolver implements RateLimiterKeyResolver {
@Override @Override
public String resolver(JoinPoint joinPoint, RateLimiter rateLimiter) { public String resolver(JoinPoint joinPoint, RateLimiter rateLimiter) {
String methodName = joinPoint.getSignature().toString(); String methodName = joinPoint.getSignature().toString();
String argsStr = StrUtil.join(",", joinPoint.getArgs()); String argsStr = StrUtils.joinMethodArgs(joinPoint);
return SecureUtil.md5(methodName + argsStr); return SecureUtil.md5(methodName + argsStr);
} }

View File

@ -1,8 +1,8 @@
package cn.iocoder.yudao.framework.ratelimiter.core.keyresolver.impl; package cn.iocoder.yudao.framework.ratelimiter.core.keyresolver.impl;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.SecureUtil; import cn.hutool.crypto.SecureUtil;
import cn.hutool.system.SystemUtil; import cn.hutool.system.SystemUtil;
import cn.iocoder.yudao.framework.common.util.string.StrUtils;
import cn.iocoder.yudao.framework.ratelimiter.core.annotation.RateLimiter; import cn.iocoder.yudao.framework.ratelimiter.core.annotation.RateLimiter;
import cn.iocoder.yudao.framework.ratelimiter.core.keyresolver.RateLimiterKeyResolver; import cn.iocoder.yudao.framework.ratelimiter.core.keyresolver.RateLimiterKeyResolver;
import org.aspectj.lang.JoinPoint; import org.aspectj.lang.JoinPoint;
@ -19,7 +19,7 @@ public class ServerNodeRateLimiterKeyResolver implements RateLimiterKeyResolver
@Override @Override
public String resolver(JoinPoint joinPoint, RateLimiter rateLimiter) { public String resolver(JoinPoint joinPoint, RateLimiter rateLimiter) {
String methodName = joinPoint.getSignature().toString(); String methodName = joinPoint.getSignature().toString();
String argsStr = StrUtil.join(",", joinPoint.getArgs()); String argsStr = StrUtils.joinMethodArgs(joinPoint);
String serverNode = String.format("%s@%d", SystemUtil.getHostInfo().getAddress(), SystemUtil.getCurrentPID()); String serverNode = String.format("%s@%d", SystemUtil.getHostInfo().getAddress(), SystemUtil.getCurrentPID());
return SecureUtil.md5(methodName + argsStr + serverNode); return SecureUtil.md5(methodName + argsStr + serverNode);
} }

View File

@ -1,7 +1,7 @@
package cn.iocoder.yudao.framework.ratelimiter.core.keyresolver.impl; package cn.iocoder.yudao.framework.ratelimiter.core.keyresolver.impl;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.SecureUtil; import cn.hutool.crypto.SecureUtil;
import cn.iocoder.yudao.framework.common.util.string.StrUtils;
import cn.iocoder.yudao.framework.ratelimiter.core.annotation.RateLimiter; import cn.iocoder.yudao.framework.ratelimiter.core.annotation.RateLimiter;
import cn.iocoder.yudao.framework.ratelimiter.core.keyresolver.RateLimiterKeyResolver; import cn.iocoder.yudao.framework.ratelimiter.core.keyresolver.RateLimiterKeyResolver;
import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils; import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils;
@ -19,7 +19,7 @@ public class UserRateLimiterKeyResolver implements RateLimiterKeyResolver {
@Override @Override
public String resolver(JoinPoint joinPoint, RateLimiter rateLimiter) { public String resolver(JoinPoint joinPoint, RateLimiter rateLimiter) {
String methodName = joinPoint.getSignature().toString(); String methodName = joinPoint.getSignature().toString();
String argsStr = StrUtil.join(",", joinPoint.getArgs()); String argsStr = StrUtils.joinMethodArgs(joinPoint);
Long userId = WebFrameworkUtils.getLoginUserId(); Long userId = WebFrameworkUtils.getLoginUserId();
Integer userType = WebFrameworkUtils.getLoginUserType(); Integer userType = WebFrameworkUtils.getLoginUserType();
return SecureUtil.md5(methodName + argsStr + userId + userType); return SecureUtil.md5(methodName + argsStr + userId + userType);

View File

@ -3,6 +3,7 @@ package cn.iocoder.yudao.framework.ratelimiter.core.redis;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import org.redisson.api.*; import org.redisson.api.*;
import java.time.Duration;
import java.util.Objects; import java.util.Objects;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -40,11 +41,13 @@ public class RateLimiterRedisDAO {
String redisKey = formatKey(key); String redisKey = formatKey(key);
RRateLimiter rateLimiter = redissonClient.getRateLimiter(redisKey); RRateLimiter rateLimiter = redissonClient.getRateLimiter(redisKey);
long rateInterval = timeUnit.toSeconds(time); long rateInterval = timeUnit.toSeconds(time);
Duration duration = Duration.ofSeconds(rateInterval);
// 1. 如果不存在设置 rate 速率 // 1. 如果不存在设置 rate 速率
RateLimiterConfig config = rateLimiter.getConfig(); RateLimiterConfig config = rateLimiter.getConfig();
if (config == null) { if (config == null) {
rateLimiter.trySetRate(RateType.OVERALL, count, rateInterval, RateIntervalUnit.SECONDS); rateLimiter.trySetRate(RateType.OVERALL, count, duration);
rateLimiter.expire(rateInterval, TimeUnit.SECONDS); // 原因参见 https://t.zsxq.com/lcR0W // 原因参见 https://t.zsxq.com/lcR0W
rateLimiter.expire(duration);
return rateLimiter; return rateLimiter;
} }
// 2. 如果存在并且配置相同则直接返回 // 2. 如果存在并且配置相同则直接返回
@ -54,8 +57,9 @@ public class RateLimiterRedisDAO {
return rateLimiter; return rateLimiter;
} }
// 3. 如果存在并且配置不同则进行新建 // 3. 如果存在并且配置不同则进行新建
rateLimiter.setRate(RateType.OVERALL, count, rateInterval, RateIntervalUnit.SECONDS); rateLimiter.setRate(RateType.OVERALL, count, duration);
rateLimiter.expire(rateInterval, TimeUnit.SECONDS); // 原因参见 https://t.zsxq.com/lcR0W // 原因参见 https://t.zsxq.com/lcR0W
rateLimiter.expire(duration);
return rateLimiter; return rateLimiter;
} }

View File

@ -178,6 +178,7 @@ public class GlobalExceptionHandler {
* 例如说接口上设置了 @RequestBody 实体中 xx 属性类型为 Integer结果传递 xx 参数类型为 String * 例如说接口上设置了 @RequestBody 实体中 xx 属性类型为 Integer结果传递 xx 参数类型为 String
*/ */
@ExceptionHandler(HttpMessageNotReadableException.class) @ExceptionHandler(HttpMessageNotReadableException.class)
@SuppressWarnings("PatternVariableCanBeUsed")
public CommonResult<?> methodArgumentTypeInvalidFormatExceptionHandler(HttpMessageNotReadableException ex) { public CommonResult<?> methodArgumentTypeInvalidFormatExceptionHandler(HttpMessageNotReadableException ex) {
log.warn("[methodArgumentTypeInvalidFormatExceptionHandler]", ex); log.warn("[methodArgumentTypeInvalidFormatExceptionHandler]", ex);
if (ex.getCause() instanceof InvalidFormatException) { if (ex.getCause() instanceof InvalidFormatException) {

View File

@ -148,6 +148,7 @@ public class WebFrameworkUtils {
return (CommonResult<?>) request.getAttribute(REQUEST_ATTRIBUTE_COMMON_RESULT); return (CommonResult<?>) request.getAttribute(REQUEST_ATTRIBUTE_COMMON_RESULT);
} }
@SuppressWarnings("PatternVariableCanBeUsed")
public static HttpServletRequest getRequest() { public static HttpServletRequest getRequest() {
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
if (!(requestAttributes instanceof ServletRequestAttributes)) { if (!(requestAttributes instanceof ServletRequestAttributes)) {

View File

@ -0,0 +1,86 @@
package cn.iocoder.yudao.framework.encrypt;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.SecureUtil;
import cn.hutool.crypto.asymmetric.AsymmetricAlgorithm;
import cn.hutool.crypto.asymmetric.KeyType;
import cn.hutool.crypto.asymmetric.RSA;
import cn.hutool.crypto.symmetric.SymmetricAlgorithm;
import org.junit.jupiter.api.Test;
import java.util.Objects;
/**
* 各种 API 加解密的测试类不是单测而是方便大家生成密钥加密解密等操作
*
* @author 芋道源码
*/
@SuppressWarnings("ConstantValue")
public class ApiEncryptTest {
@Test
public void testGenerateAsymmetric() {
String asymmetricAlgorithm = AsymmetricAlgorithm.RSA.getValue();
// String asymmetricAlgorithm = "SM2";
// String asymmetricAlgorithm = SM4.ALGORITHM_NAME;
// String asymmetricAlgorithm = SymmetricAlgorithm.AES.getValue();
String requestClientKey = null;
String requestServerKey = null;
String responseClientKey = null;
String responseServerKey = null;
if (Objects.equals(asymmetricAlgorithm, AsymmetricAlgorithm.RSA.getValue())) {
// 请求的密钥
RSA requestRsa = SecureUtil.rsa();
requestClientKey = requestRsa.getPublicKeyBase64();
requestServerKey = requestRsa.getPrivateKeyBase64();
// 响应的密钥
RSA responseRsa = new RSA();
responseClientKey = responseRsa.getPrivateKeyBase64();
responseServerKey = responseRsa.getPublicKeyBase64();
} else if (Objects.equals(asymmetricAlgorithm, SymmetricAlgorithm.AES.getValue())) {
// AES 密钥可选 322416
// 请求的密钥前后端密钥一致
requestClientKey = RandomUtil.randomNumbers(32);
requestServerKey = requestClientKey;
// 响应的密钥前后端密钥一致
responseClientKey = RandomUtil.randomNumbers(32);
responseServerKey = responseClientKey;
}
// 打印结果
System.out.println("requestClientKey = " + requestClientKey);
System.out.println("requestServerKey = " + requestServerKey);
System.out.println("responseClientKey = " + responseClientKey);
System.out.println("responseServerKey = " + responseServerKey);
}
@Test
public void testEncrypt_aes() {
String key = "52549111389893486934626385991395";
String body = "{\n" +
" \"username\": \"admin\",\n" +
" \"password\": \"admin123\",\n" +
" \"uuid\": \"3acd87a09a4f48fb9118333780e94883\",\n" +
" \"code\": \"1024\"\n" +
"}";
String encrypt = SecureUtil.aes(StrUtil.utf8Bytes(key))
.encryptBase64(body);
System.out.println("encrypt = " + encrypt);
}
@Test
public void testEncrypt_rsa() {
String key = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCls2rIpnGdYnLFgz1XU13GbNQ5DloyPpvW00FPGjqn5Z6JpK+kDtVlnkhwR87iRrE5Vf2WNqRX6vzbLSgveIQY8e8oqGCb829myjf1MuI+ZzN4ghf/7tEYhZJGPI9AbfxFqBUzm+kR3/HByAI22GLT96WM26QiMK8n3tIP/yiLswIDAQAB";
String body = "{\n" +
" \"username\": \"admin\",\n" +
" \"password\": \"admin123\",\n" +
" \"uuid\": \"3acd87a09a4f48fb9118333780e94883\",\n" +
" \"code\": \"1024\"\n" +
"}";
String encrypt = SecureUtil.rsa(null, key)
.encryptBase64(body, KeyType.PublicKey);
System.out.println("encrypt = " + encrypt);
}
}

View File

@ -181,6 +181,10 @@ spring:
- Path=/admin-api/ai/** - Path=/admin-api/ai/**
filters: filters:
- RewritePath=/admin-api/ai/v3/api-docs, /v3/api-docs # 配置,保证转发到 /v3/api-docs - RewritePath=/admin-api/ai/v3/api-docs, /v3/api-docs # 配置,保证转发到 /v3/api-docs
- id: ai-mcp-server # 路由的编号MCP Server
uri: grayLb://ai-server
predicates: # 断言,作为路由的匹配条件,对应 RouteDefinition 数组
- Path=/sse, /mcp/message
## iot-server 服务 ## iot-server 服务
- id: iot-admin-api # 路由的编号 - id: iot-admin-api # 路由的编号
uri: grayLb://iot-server uri: grayLb://iot-server

View File

@ -33,6 +33,8 @@ public enum AiPlatformEnum implements ArrayValuable<String> {
OPENAI("OpenAI", "OpenAI"), // OpenAI 官方 OPENAI("OpenAI", "OpenAI"), // OpenAI 官方
AZURE_OPENAI("AzureOpenAI", "AzureOpenAI"), // OpenAI 微软 AZURE_OPENAI("AzureOpenAI", "AzureOpenAI"), // OpenAI 微软
ANTHROPIC("Anthropic", "Anthropic"), // Anthropic Claude
GEMINI("Gemini", "Gemini"), // 谷歌 Gemini
OLLAMA("Ollama", "Ollama"), OLLAMA("Ollama", "Ollama"),
STABLE_DIFFUSION("StableDiffusion", "StableDiffusion"), // Stability AI STABLE_DIFFUSION("StableDiffusion", "StableDiffusion"), // Stability AI

View File

@ -9,6 +9,7 @@
</parent> </parent>
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<artifactId>yudao-module-ai-server</artifactId> <artifactId>yudao-module-ai-server</artifactId>
<packaging>jar</packaging>
<name>${project.artifactId}</name> <name>${project.artifactId}</name>
<description> <description>
@ -18,8 +19,8 @@
国外OpenAI、Ollama、Midjourney、StableDiffusion、Suno 国外OpenAI、Ollama、Midjourney、StableDiffusion、Suno
</description> </description>
<properties> <properties>
<spring-ai.version>1.0.0</spring-ai.version> <spring-ai.version>1.0.1</spring-ai.version>
<alibaba-ai.version>1.0.0.2</alibaba-ai.version> <alibaba-ai.version>1.0.0.3</alibaba-ai.version>
<tinyflow.version>1.0.2</tinyflow.version> <tinyflow.version>1.0.2</tinyflow.version>
</properties> </properties>
@ -119,6 +120,11 @@
<artifactId>spring-ai-starter-model-azure-openai</artifactId> <artifactId>spring-ai-starter-model-azure-openai</artifactId>
<version>${spring-ai.version}</version> <version>${spring-ai.version}</version>
</dependency> </dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-anthropic</artifactId>
<version>${spring-ai.version}</version>
</dependency>
<dependency> <dependency>
<groupId>org.springframework.ai</groupId> <groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-deepseek</artifactId> <artifactId>spring-ai-starter-model-deepseek</artifactId>
@ -217,6 +223,24 @@
</exclusions> </exclusions>
</dependency> </dependency>
<!-- MCP 相关 -->
<!--
特殊说明:不能使用 spring-ai-starter-mcp-server-webflux 或 spring-ai-starter-mcp-client-webflux
原因:项目使用了 SpringMVC而不是 WebFlux。引入上述 2 个,会导致 SSE Server 失效。
-->
<dependency>
<!-- 服务端 -->
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-server-webmvc</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>
<!-- TinyFlowAI 工作流 --> <!-- TinyFlowAI 工作流 -->
<dependency> <dependency>
<groupId>dev.tinyflow</groupId> <groupId>dev.tinyflow</groupId>

View File

@ -20,9 +20,46 @@ tenant-id: {{adminTenantId}}
"content": "1+1=?" "content": "1+1=?"
} }
### 获得指定对话的消息列表 ### 发送消息(流式)【带文件】
GET {{baseUrl}}/ai/chat/message/list-by-conversation-id?conversationId=1781604279872581649 POST {{baseUrl}}/ai/chat/message/send-stream
Content-Type: application/json
Authorization: {{token}} Authorization: {{token}}
tenant-id: {{adminTenantId}}
{
"conversationId": "1781604279872581797",
"content": "图片里有什么?",
"attachmentUrls": ["http://test.yudao.iocoder.cn/1755531278.jpeg"]
}
### 发送消息(流式)【追问带文件】
POST {{baseUrl}}/ai/chat/message/send-stream
Content-Type: application/json
Authorization: {{token}}
tenant-id: {{adminTenantId}}
{
"conversationId": "1781604279872581799",
"content": "说下图片里,有哪些字?",
"useContext": true
}
### 发送消息(流式)【联网搜索】
POST {{baseUrl}}/ai/chat/message/send-stream
Content-Type: application/json
Authorization: {{token}}
tenant-id: {{adminTenantId}}
{
"conversationId": "1781604279872581799",
"content": "今天是周几?",
"useSearch": true
}
### 获得指定对话的消息列表
GET {{baseUrl}}/ai/chat/message/list-by-conversation-id?conversationId=1781604279872581799
Authorization: {{token}}
tenant-id: {{adminTenantId}}
### 删除消息 ### 删除消息
DELETE {{baseUrl}}/ai/chat/message/delete?id=50 DELETE {{baseUrl}}/ai/chat/message/delete?id=50

View File

@ -1,5 +1,6 @@
package cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message; package cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message;
import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.AiWebSearchResponse;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
@ -37,6 +38,9 @@ public class AiChatMessageRespVO {
@Schema(description = "聊天内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "你好,你好啊") @Schema(description = "聊天内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "你好,你好啊")
private String content; private String content;
@Schema(description = "推理内容", example = "要达到这个目标,你需要...")
private String reasoningContent;
@Schema(description = "是否携带上下文", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") @Schema(description = "是否携带上下文", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
private Boolean useContext; private Boolean useContext;
@ -46,6 +50,12 @@ public class AiChatMessageRespVO {
@Schema(description = "知识库段落数组") @Schema(description = "知识库段落数组")
private List<KnowledgeSegment> segments; private List<KnowledgeSegment> segments;
@Schema(description = "联网搜索的网页内容数组")
private List<AiWebSearchResponse.WebPage> webSearchPages;
@Schema(description = "附件 URL 数组", example = "https://www.iocoder.cn/1.png")
private List<String> attachmentUrls;
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "2024-05-12 12:51") @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "2024-05-12 12:51")
private LocalDateTime createTime; private LocalDateTime createTime;

View File

@ -3,9 +3,9 @@ package cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data; import lombok.Data;
import lombok.experimental.Accessors;
import java.util.List;
@Schema(description = "管理后台 - AI 聊天消息发送 Request VO") @Schema(description = "管理后台 - AI 聊天消息发送 Request VO")
@Data @Data
@ -22,4 +22,10 @@ public class AiChatMessageSendReqVO {
@Schema(description = "是否携带上下文", example = "true") @Schema(description = "是否携带上下文", example = "true")
private Boolean useContext; private Boolean useContext;
@Schema(description = "是否联网搜索", example = "true")
private Boolean useSearch;
@Schema(description = "附件 URL 数组", example = "https://www.iocoder.cn/1.png")
private List<String> attachmentUrls;
} }

View File

@ -1,5 +1,6 @@
package cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message; package cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message;
import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.AiWebSearchResponse;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
@ -29,12 +30,18 @@ public class AiChatMessageSendRespVO {
@Schema(description = "聊天内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "你好,你好啊") @Schema(description = "聊天内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "你好,你好啊")
private String content; private String content;
@Schema(description = "推理内容", example = "要达到这个目标,你需要...")
private String reasoningContent;
@Schema(description = "知识库段落编号数组", example = "[1,2,3]") @Schema(description = "知识库段落编号数组", example = "[1,2,3]")
private List<Long> segmentIds; private List<Long> segmentIds;
@Schema(description = "知识库段落数组") @Schema(description = "知识库段落数组")
private List<AiChatMessageRespVO.KnowledgeSegment> segments; private List<AiChatMessageRespVO.KnowledgeSegment> segments;
@Schema(description = "联网搜索的网页内容数组")
private List<AiWebSearchResponse.WebPage> webSearchPages;
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDateTime createTime; private LocalDateTime createTime;

View File

@ -52,6 +52,9 @@ public class AiChatRoleRespVO implements VO {
@Schema(description = "引用的工具编号列表", example = "1,2,3") @Schema(description = "引用的工具编号列表", example = "1,2,3")
private List<Long> toolIds; private List<Long> toolIds;
@Schema(description = "引用的 MCP Client 名字列表", example = "filesystem")
private List<String> mcpClientNames;
@Schema(description = "是否公开", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") @Schema(description = "是否公开", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Boolean publicStatus; private Boolean publicStatus;

View File

@ -37,4 +37,7 @@ public class AiChatRoleSaveMyReqVO {
@Schema(description = "引用的工具编号列表", example = "1,2,3") @Schema(description = "引用的工具编号列表", example = "1,2,3")
private List<Long> toolIds; private List<Long> toolIds;
@Schema(description = "引用的 MCP Client 名字列表", example = "filesystem")
private List<String> mcpClientNames;
} }

View File

@ -50,6 +50,9 @@ public class AiChatRoleSaveReqVO {
@Schema(description = "引用的工具编号列表", example = "1,2,3") @Schema(description = "引用的工具编号列表", example = "1,2,3")
private List<Long> toolIds; private List<Long> toolIds;
@Schema(description = "引用的 MCP Client 名字列表", example = "filesystem")
private List<String> mcpClientNames;
@Schema(description = "是否公开", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") @Schema(description = "是否公开", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotNull(message = "是否公开不能为空") @NotNull(message = "是否公开不能为空")
private Boolean publicStatus; private Boolean publicStatus;

View File

@ -2,14 +2,20 @@ package cn.iocoder.yudao.module.ai.dal.dataobject.chat;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.framework.mybatis.core.type.LongListTypeHandler; import cn.iocoder.yudao.framework.mybatis.core.type.LongListTypeHandler;
import cn.iocoder.yudao.framework.mybatis.core.type.StringListTypeHandler;
import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeSegmentDO; import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeSegmentDO;
import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatRoleDO; import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatRoleDO;
import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiModelDO; import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiModelDO;
import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.AiWebSearchResponse;
import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*; import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.ai.chat.messages.MessageType; import org.springframework.ai.chat.messages.MessageType;
import java.util.List; import java.util.List;
@ -87,6 +93,10 @@ public class AiChatMessageDO extends BaseDO {
* 聊天内容 * 聊天内容
*/ */
private String content; private String content;
/**
* 推理内容
*/
private String reasoningContent;
/** /**
* 是否携带上下文 * 是否携带上下文
@ -101,4 +111,16 @@ public class AiChatMessageDO extends BaseDO {
@TableField(typeHandler = LongListTypeHandler.class) @TableField(typeHandler = LongListTypeHandler.class)
private List<Long> segmentIds; private List<Long> segmentIds;
/**
* 联网搜索的网页内容数组
*/
@TableField(typeHandler = JacksonTypeHandler.class)
private List<AiWebSearchResponse.WebPage> webSearchPages;
/**
* 附件 URL 数组
*/
@TableField(typeHandler = StringListTypeHandler.class)
private List<String> attachmentUrls;
} }

View File

@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.ai.dal.dataobject.model;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.framework.mybatis.core.type.LongListTypeHandler; import cn.iocoder.yudao.framework.mybatis.core.type.LongListTypeHandler;
import cn.iocoder.yudao.framework.mybatis.core.type.StringListTypeHandler;
import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDO; import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDO;
import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableField;
@ -80,6 +81,13 @@ public class AiChatRoleDO extends BaseDO {
*/ */
@TableField(typeHandler = LongListTypeHandler.class) @TableField(typeHandler = LongListTypeHandler.class)
private List<Long> toolIds; private List<Long> toolIds;
/**
* 引用的 MCP Client 名字列表
*
* 关联 spring.ai.mcp.client 下的名字
*/
@TableField(typeHandler = StringListTypeHandler.class)
private List<String> mcpClientNames;
/** /**
* 是否公开 * 是否公开

View File

@ -1,8 +1,8 @@
package cn.iocoder.yudao.module.ai.dal.dataobject.model; package cn.iocoder.yudao.module.ai.dal.dataobject.model;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.module.ai.service.model.tool.DirectoryListToolFunction; import cn.iocoder.yudao.module.ai.tool.function.DirectoryListToolFunction;
import cn.iocoder.yudao.module.ai.service.model.tool.WeatherQueryToolFunction; import cn.iocoder.yudao.module.ai.tool.function.WeatherQueryToolFunction;
import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.annotation.TableName;

View File

@ -2,25 +2,34 @@ package cn.iocoder.yudao.module.ai.framework.ai.config;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil; import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.module.ai.framework.ai.core.AiModelFactory; import cn.iocoder.yudao.module.ai.framework.ai.core.model.AiModelFactory;
import cn.iocoder.yudao.module.ai.framework.ai.core.AiModelFactoryImpl; 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.baichuan.BaiChuanChatModel;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.doubao.DouBaoChatModel; 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.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.midjourney.api.MidjourneyApi;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.siliconflow.SiliconFlowApiConstants; 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.SiliconFlowChatModel;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.suno.api.SunoApi; 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.xinghuo.XingHuoChatModel;
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 lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.deepseek.DeepSeekChatModel;
import org.springframework.ai.deepseek.DeepSeekChatOptions;
import org.springframework.ai.deepseek.api.DeepSeekApi;
import org.springframework.ai.embedding.BatchingStrategy; import org.springframework.ai.embedding.BatchingStrategy;
import org.springframework.ai.embedding.TokenCountBatchingStrategy; import org.springframework.ai.embedding.TokenCountBatchingStrategy;
import org.springframework.ai.model.tool.ToolCallingManager; import org.springframework.ai.model.tool.ToolCallingManager;
import org.springframework.ai.openai.OpenAiChatModel; import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.ai.openai.OpenAiChatOptions; import org.springframework.ai.openai.OpenAiChatOptions;
import org.springframework.ai.openai.api.OpenAiApi; import org.springframework.ai.openai.api.OpenAiApi;
import org.springframework.ai.support.ToolCallbacks;
import org.springframework.ai.tokenizer.JTokkitTokenCountEstimator; import org.springframework.ai.tokenizer.JTokkitTokenCountEstimator;
import org.springframework.ai.tokenizer.TokenCountEstimator; import org.springframework.ai.tokenizer.TokenCountEstimator;
import org.springframework.ai.tool.ToolCallback;
import org.springframework.ai.vectorstore.milvus.autoconfigure.MilvusServiceClientProperties; import org.springframework.ai.vectorstore.milvus.autoconfigure.MilvusServiceClientProperties;
import org.springframework.ai.vectorstore.milvus.autoconfigure.MilvusVectorStoreProperties; import org.springframework.ai.vectorstore.milvus.autoconfigure.MilvusVectorStoreProperties;
import org.springframework.ai.vectorstore.qdrant.autoconfigure.QdrantVectorStoreProperties; import org.springframework.ai.vectorstore.qdrant.autoconfigure.QdrantVectorStoreProperties;
@ -30,6 +39,8 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import java.util.List;
/** /**
* 芋道 AI 自动配置 * 芋道 AI 自动配置
* *
@ -51,20 +62,49 @@ public class AiAutoConfiguration {
// ========== 各种 AI Client 创建 ========== // ========== 各种 AI Client 创建 ==========
@Bean
@ConditionalOnProperty(value = "yudao.ai.gemini.enable", havingValue = "true")
public GeminiChatModel geminiChatModel(YudaoAiProperties yudaoAiProperties) {
YudaoAiProperties.Gemini properties = yudaoAiProperties.getGemini();
return buildGeminiChatClient(properties);
}
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())
.build();
return new GeminiChatModel(openAiChatModel);
}
@Bean @Bean
@ConditionalOnProperty(value = "yudao.ai.doubao.enable", havingValue = "true") @ConditionalOnProperty(value = "yudao.ai.doubao.enable", havingValue = "true")
public DouBaoChatModel douBaoChatClient(YudaoAiProperties yudaoAiProperties) { public DouBaoChatModel douBaoChatClient(YudaoAiProperties yudaoAiProperties) {
YudaoAiProperties.DouBaoProperties properties = yudaoAiProperties.getDoubao(); YudaoAiProperties.DouBao properties = yudaoAiProperties.getDoubao();
return buildDouBaoChatClient(properties); return buildDouBaoChatClient(properties);
} }
public DouBaoChatModel buildDouBaoChatClient(YudaoAiProperties.DouBaoProperties properties) { public DouBaoChatModel buildDouBaoChatClient(YudaoAiProperties.DouBao properties) {
if (StrUtil.isEmpty(properties.getModel())) { if (StrUtil.isEmpty(properties.getModel())) {
properties.setModel(DouBaoChatModel.MODEL_DEFAULT); properties.setModel(DouBaoChatModel.MODEL_DEFAULT);
} }
OpenAiChatModel openAiChatModel = OpenAiChatModel.builder() OpenAiChatModel openAiChatModel = OpenAiChatModel.builder()
.openAiApi(OpenAiApi.builder() .openAiApi(OpenAiApi.builder()
.baseUrl(DouBaoChatModel.BASE_URL) .baseUrl(DouBaoChatModel.BASE_URL)
.completionsPath(DouBaoChatModel.COMPLETE_PATH)
.apiKey(properties.getApiKey()) .apiKey(properties.getApiKey())
.build()) .build())
.defaultOptions(OpenAiChatOptions.builder() .defaultOptions(OpenAiChatOptions.builder()
@ -81,20 +121,20 @@ public class AiAutoConfiguration {
@Bean @Bean
@ConditionalOnProperty(value = "yudao.ai.siliconflow.enable", havingValue = "true") @ConditionalOnProperty(value = "yudao.ai.siliconflow.enable", havingValue = "true")
public SiliconFlowChatModel siliconFlowChatClient(YudaoAiProperties yudaoAiProperties) { public SiliconFlowChatModel siliconFlowChatClient(YudaoAiProperties yudaoAiProperties) {
YudaoAiProperties.SiliconFlowProperties properties = yudaoAiProperties.getSiliconflow(); YudaoAiProperties.SiliconFlow properties = yudaoAiProperties.getSiliconflow();
return buildSiliconFlowChatClient(properties); return buildSiliconFlowChatClient(properties);
} }
public SiliconFlowChatModel buildSiliconFlowChatClient(YudaoAiProperties.SiliconFlowProperties properties) { public SiliconFlowChatModel buildSiliconFlowChatClient(YudaoAiProperties.SiliconFlow properties) {
if (StrUtil.isEmpty(properties.getModel())) { if (StrUtil.isEmpty(properties.getModel())) {
properties.setModel(SiliconFlowApiConstants.MODEL_DEFAULT); properties.setModel(SiliconFlowApiConstants.MODEL_DEFAULT);
} }
OpenAiChatModel openAiChatModel = OpenAiChatModel.builder() DeepSeekChatModel openAiChatModel = DeepSeekChatModel.builder()
.openAiApi(OpenAiApi.builder() .deepSeekApi(DeepSeekApi.builder()
.baseUrl(SiliconFlowApiConstants.DEFAULT_BASE_URL) .baseUrl(SiliconFlowApiConstants.DEFAULT_BASE_URL)
.apiKey(properties.getApiKey()) .apiKey(properties.getApiKey())
.build()) .build())
.defaultOptions(OpenAiChatOptions.builder() .defaultOptions(DeepSeekChatOptions.builder()
.model(properties.getModel()) .model(properties.getModel())
.temperature(properties.getTemperature()) .temperature(properties.getTemperature())
.maxTokens(properties.getMaxTokens()) .maxTokens(properties.getMaxTokens())
@ -108,11 +148,11 @@ public class AiAutoConfiguration {
@Bean @Bean
@ConditionalOnProperty(value = "yudao.ai.hunyuan.enable", havingValue = "true") @ConditionalOnProperty(value = "yudao.ai.hunyuan.enable", havingValue = "true")
public HunYuanChatModel hunYuanChatClient(YudaoAiProperties yudaoAiProperties) { public HunYuanChatModel hunYuanChatClient(YudaoAiProperties yudaoAiProperties) {
YudaoAiProperties.HunYuanProperties properties = yudaoAiProperties.getHunyuan(); YudaoAiProperties.HunYuan properties = yudaoAiProperties.getHunyuan();
return buildHunYuanChatClient(properties); return buildHunYuanChatClient(properties);
} }
public HunYuanChatModel buildHunYuanChatClient(YudaoAiProperties.HunYuanProperties properties) { public HunYuanChatModel buildHunYuanChatClient(YudaoAiProperties.HunYuan properties) {
if (StrUtil.isEmpty(properties.getModel())) { if (StrUtil.isEmpty(properties.getModel())) {
properties.setModel(HunYuanChatModel.MODEL_DEFAULT); properties.setModel(HunYuanChatModel.MODEL_DEFAULT);
} }
@ -122,13 +162,14 @@ public class AiAutoConfiguration {
StrUtil.startWithIgnoreCase(properties.getModel(), "deepseek") ? HunYuanChatModel.DEEP_SEEK_BASE_URL StrUtil.startWithIgnoreCase(properties.getModel(), "deepseek") ? HunYuanChatModel.DEEP_SEEK_BASE_URL
: HunYuanChatModel.BASE_URL); : HunYuanChatModel.BASE_URL);
} }
// 创建 OpenAiChatModelHunYuanChatModel 对象 // 创建 DeepSeekChatModelHunYuanChatModel 对象
OpenAiChatModel openAiChatModel = OpenAiChatModel.builder() DeepSeekChatModel openAiChatModel = DeepSeekChatModel.builder()
.openAiApi(OpenAiApi.builder() .deepSeekApi(DeepSeekApi.builder()
.baseUrl(properties.getBaseUrl()) .baseUrl(properties.getBaseUrl())
.completionsPath(HunYuanChatModel.COMPLETE_PATH)
.apiKey(properties.getApiKey()) .apiKey(properties.getApiKey())
.build()) .build())
.defaultOptions(OpenAiChatOptions.builder() .defaultOptions(DeepSeekChatOptions.builder()
.model(properties.getModel()) .model(properties.getModel())
.temperature(properties.getTemperature()) .temperature(properties.getTemperature())
.maxTokens(properties.getMaxTokens()) .maxTokens(properties.getMaxTokens())
@ -142,25 +183,30 @@ public class AiAutoConfiguration {
@Bean @Bean
@ConditionalOnProperty(value = "yudao.ai.xinghuo.enable", havingValue = "true") @ConditionalOnProperty(value = "yudao.ai.xinghuo.enable", havingValue = "true")
public XingHuoChatModel xingHuoChatClient(YudaoAiProperties yudaoAiProperties) { public XingHuoChatModel xingHuoChatClient(YudaoAiProperties yudaoAiProperties) {
YudaoAiProperties.XingHuoProperties properties = yudaoAiProperties.getXinghuo(); YudaoAiProperties.XingHuo properties = yudaoAiProperties.getXinghuo();
return buildXingHuoChatClient(properties); return buildXingHuoChatClient(properties);
} }
public XingHuoChatModel buildXingHuoChatClient(YudaoAiProperties.XingHuoProperties properties) { public XingHuoChatModel buildXingHuoChatClient(YudaoAiProperties.XingHuo properties) {
if (StrUtil.isEmpty(properties.getModel())) { if (StrUtil.isEmpty(properties.getModel())) {
properties.setModel(XingHuoChatModel.MODEL_DEFAULT); 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() OpenAiChatModel openAiChatModel = OpenAiChatModel.builder()
.openAiApi(OpenAiApi.builder() .openAiApi(builder.build())
.baseUrl(XingHuoChatModel.BASE_URL)
.apiKey(properties.getAppKey() + ":" + properties.getSecretKey())
.build())
.defaultOptions(OpenAiChatOptions.builder() .defaultOptions(OpenAiChatOptions.builder()
.model(properties.getModel()) .model(properties.getModel())
.temperature(properties.getTemperature()) .temperature(properties.getTemperature())
.maxTokens(properties.getMaxTokens()) .maxTokens(properties.getMaxTokens())
.topP(properties.getTopP()) .topP(properties.getTopP())
.build()) .build())
// TODO @芋艿星火的 function call bug会报 ToolResponseMessage must have an id 错误
.toolCallingManager(getToolCallingManager()) .toolCallingManager(getToolCallingManager())
.build(); .build();
return new XingHuoChatModel(openAiChatModel); return new XingHuoChatModel(openAiChatModel);
@ -169,11 +215,11 @@ public class AiAutoConfiguration {
@Bean @Bean
@ConditionalOnProperty(value = "yudao.ai.baichuan.enable", havingValue = "true") @ConditionalOnProperty(value = "yudao.ai.baichuan.enable", havingValue = "true")
public BaiChuanChatModel baiChuanChatClient(YudaoAiProperties yudaoAiProperties) { public BaiChuanChatModel baiChuanChatClient(YudaoAiProperties yudaoAiProperties) {
YudaoAiProperties.BaiChuanProperties properties = yudaoAiProperties.getBaichuan(); YudaoAiProperties.BaiChuan properties = yudaoAiProperties.getBaichuan();
return buildBaiChuanChatClient(properties); return buildBaiChuanChatClient(properties);
} }
public BaiChuanChatModel buildBaiChuanChatClient(YudaoAiProperties.BaiChuanProperties properties) { public BaiChuanChatModel buildBaiChuanChatClient(YudaoAiProperties.BaiChuan properties) {
if (StrUtil.isEmpty(properties.getModel())) { if (StrUtil.isEmpty(properties.getModel())) {
properties.setModel(BaiChuanChatModel.MODEL_DEFAULT); properties.setModel(BaiChuanChatModel.MODEL_DEFAULT);
} }
@ -196,7 +242,7 @@ public class AiAutoConfiguration {
@Bean @Bean
@ConditionalOnProperty(value = "yudao.ai.midjourney.enable", havingValue = "true") @ConditionalOnProperty(value = "yudao.ai.midjourney.enable", havingValue = "true")
public MidjourneyApi midjourneyApi(YudaoAiProperties yudaoAiProperties) { public MidjourneyApi midjourneyApi(YudaoAiProperties yudaoAiProperties) {
YudaoAiProperties.MidjourneyProperties config = yudaoAiProperties.getMidjourney(); YudaoAiProperties.Midjourney config = yudaoAiProperties.getMidjourney();
return new MidjourneyApi(config.getBaseUrl(), config.getApiKey(), config.getNotifyUrl()); return new MidjourneyApi(config.getBaseUrl(), config.getApiKey(), config.getNotifyUrl());
} }
@ -222,4 +268,22 @@ public class AiAutoConfiguration {
return SpringUtil.getBean(ToolCallingManager.class); return SpringUtil.getBean(ToolCallingManager.class);
} }
// ========== Web Search 相关 ==========
@Bean
@ConditionalOnProperty(value = "yudao.ai.web-search.enable", havingValue = "true")
public AiWebSearchClient webSearchClient(YudaoAiProperties yudaoAiProperties) {
return new AiBoChaWebSearchClient(yudaoAiProperties.getWebSearch().getApiKey());
}
// ========== MCP 相关 ==========
/**
* 参考自 <a href="https://docs.spring.io/spring-ai/reference/api/mcp/mcp-client-boot-starter-docs.html">MCP Server Boot Starter</>
*/
@Bean
public List<ToolCallback> toolCallbacks(PersonService personService) {
return List.of(ToolCallbacks.from(personService));
}
} }

View File

@ -13,49 +13,54 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
@Data @Data
public class YudaoAiProperties { public class YudaoAiProperties {
/**
* 谷歌 Gemini
*/
private Gemini gemini;
/** /**
* 字节豆包 * 字节豆包
*/ */
@SuppressWarnings("SpellCheckingInspection") private DouBao doubao;
private DouBaoProperties doubao;
/** /**
* 腾讯混元 * 腾讯混元
*/ */
@SuppressWarnings("SpellCheckingInspection") private HunYuan hunyuan;
private HunYuanProperties hunyuan;
/** /**
* 硅基流动 * 硅基流动
*/ */
@SuppressWarnings("SpellCheckingInspection") private SiliconFlow siliconflow;
private SiliconFlowProperties siliconflow;
/** /**
* 讯飞星火 * 讯飞星火
*/ */
@SuppressWarnings("SpellCheckingInspection") private XingHuo xinghuo;
private XingHuoProperties xinghuo;
/** /**
* 百川 * 百川
*/ */
@SuppressWarnings("SpellCheckingInspection") private BaiChuan baichuan;
private BaiChuanProperties baichuan;
/** /**
* Midjourney 绘图 * Midjourney 绘图
*/ */
private MidjourneyProperties midjourney; private Midjourney midjourney;
/** /**
* Suno 音乐 * Suno 音乐
*/ */
@SuppressWarnings("SpellCheckingInspection") @SuppressWarnings("SpellCheckingInspection")
private SunoProperties suno; private Suno suno;
/**
* 网络搜索
*/
private WebSearch webSearch;
@Data @Data
public static class DouBaoProperties { public static class Gemini {
private String enable; private String enable;
private String apiKey; private String apiKey;
@ -68,7 +73,20 @@ public class YudaoAiProperties {
} }
@Data @Data
public static class HunYuanProperties { public static class DouBao {
private String enable;
private String apiKey;
private String model;
private Double temperature;
private Integer maxTokens;
private Double topP;
}
@Data
public static class HunYuan {
private String enable; private String enable;
private String baseUrl; private String baseUrl;
@ -82,7 +100,7 @@ public class YudaoAiProperties {
} }
@Data @Data
public static class SiliconFlowProperties { public static class SiliconFlow {
private String enable; private String enable;
private String apiKey; private String apiKey;
@ -95,7 +113,7 @@ public class YudaoAiProperties {
} }
@Data @Data
public static class XingHuoProperties { public static class XingHuo {
private String enable; private String enable;
private String appId; private String appId;
@ -110,7 +128,7 @@ public class YudaoAiProperties {
} }
@Data @Data
public static class BaiChuanProperties { public static class BaiChuan {
private String enable; private String enable;
private String apiKey; private String apiKey;
@ -123,7 +141,7 @@ public class YudaoAiProperties {
} }
@Data @Data
public static class MidjourneyProperties { public static class Midjourney {
private String enable; private String enable;
private String baseUrl; private String baseUrl;
@ -134,12 +152,21 @@ public class YudaoAiProperties {
} }
@Data @Data
public static class SunoProperties { public static class Suno {
private boolean enable = false; private boolean enable;
private String baseUrl; private String baseUrl;
} }
@Data
public static class WebSearch {
private boolean enable;
private String apiKey;
}
} }

View File

@ -1,4 +1,4 @@
package cn.iocoder.yudao.module.ai.framework.ai.core; package cn.iocoder.yudao.module.ai.framework.ai.core.model;
import cn.iocoder.yudao.module.ai.enums.model.AiPlatformEnum; import cn.iocoder.yudao.module.ai.enums.model.AiPlatformEnum;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.midjourney.api.MidjourneyApi; import cn.iocoder.yudao.module.ai.framework.ai.core.model.midjourney.api.MidjourneyApi;

View File

@ -1,4 +1,4 @@
package cn.iocoder.yudao.module.ai.framework.ai.core; package cn.iocoder.yudao.module.ai.framework.ai.core.model;
import cn.hutool.core.io.FileUtil; import cn.hutool.core.io.FileUtil;
import cn.hutool.core.lang.Assert; import cn.hutool.core.lang.Assert;
@ -14,6 +14,7 @@ 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.config.YudaoAiProperties;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.baichuan.BaiChuanChatModel; 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.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.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.midjourney.api.MidjourneyApi;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.siliconflow.SiliconFlowApiConstants; import cn.iocoder.yudao.module.ai.framework.ai.core.model.siliconflow.SiliconFlowApiConstants;
@ -67,6 +68,7 @@ import org.springframework.ai.minimax.MiniMaxChatOptions;
import org.springframework.ai.minimax.MiniMaxEmbeddingModel; import org.springframework.ai.minimax.MiniMaxEmbeddingModel;
import org.springframework.ai.minimax.MiniMaxEmbeddingOptions; import org.springframework.ai.minimax.MiniMaxEmbeddingOptions;
import org.springframework.ai.minimax.api.MiniMaxApi; 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.AzureOpenAiChatAutoConfiguration;
import org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiEmbeddingAutoConfiguration; import org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiEmbeddingAutoConfiguration;
import org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiEmbeddingProperties; import org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiEmbeddingProperties;
@ -93,6 +95,8 @@ import org.springframework.ai.openai.OpenAiImageModel;
import org.springframework.ai.openai.api.OpenAiApi; import org.springframework.ai.openai.api.OpenAiApi;
import org.springframework.ai.openai.api.OpenAiImageApi; import org.springframework.ai.openai.api.OpenAiImageApi;
import org.springframework.ai.openai.api.common.OpenAiApiConstants; 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.stabilityai.StabilityAiImageModel; import org.springframework.ai.stabilityai.StabilityAiImageModel;
import org.springframework.ai.stabilityai.api.StabilityAiApi; import org.springframework.ai.stabilityai.api.StabilityAiApi;
import org.springframework.ai.vectorstore.SimpleVectorStore; import org.springframework.ai.vectorstore.SimpleVectorStore;
@ -168,6 +172,10 @@ public class AiModelFactoryImpl implements AiModelFactory {
return buildOpenAiChatModel(apiKey, url); return buildOpenAiChatModel(apiKey, url);
case AZURE_OPENAI: case AZURE_OPENAI:
return buildAzureOpenAiChatModel(apiKey, url); return buildAzureOpenAiChatModel(apiKey, url);
case ANTHROPIC:
return buildAnthropicChatModel(apiKey, url);
case GEMINI:
return buildGeminiChatModel(apiKey);
case OLLAMA: case OLLAMA:
return buildOllamaChatModel(url); return buildOllamaChatModel(url);
default: default:
@ -206,6 +214,10 @@ public class AiModelFactoryImpl implements AiModelFactory {
return SpringUtil.getBean(OpenAiChatModel.class); return SpringUtil.getBean(OpenAiChatModel.class);
case AZURE_OPENAI: case AZURE_OPENAI:
return SpringUtil.getBean(AzureOpenAiChatModel.class); return SpringUtil.getBean(AzureOpenAiChatModel.class);
case ANTHROPIC:
return SpringUtil.getBean(AnthropicChatModel.class);
case GEMINI:
return SpringUtil.getBean(GeminiChatModel.class);
case OLLAMA: case OLLAMA:
return SpringUtil.getBean(OllamaChatModel.class); return SpringUtil.getBean(OllamaChatModel.class);
default: default:
@ -260,7 +272,7 @@ public class AiModelFactoryImpl implements AiModelFactory {
String cacheKey = buildClientCacheKey(MidjourneyApi.class, AiPlatformEnum.MIDJOURNEY.getPlatform(), apiKey, String cacheKey = buildClientCacheKey(MidjourneyApi.class, AiPlatformEnum.MIDJOURNEY.getPlatform(), apiKey,
url); url);
return Singleton.get(cacheKey, (Func0<MidjourneyApi>) () -> { return Singleton.get(cacheKey, (Func0<MidjourneyApi>) () -> {
YudaoAiProperties.MidjourneyProperties properties = SpringUtil.getBean(YudaoAiProperties.class) YudaoAiProperties.Midjourney properties = SpringUtil.getBean(YudaoAiProperties.class)
.getMidjourney(); .getMidjourney();
return new MidjourneyApi(url, apiKey, properties.getNotifyUrl()); return new MidjourneyApi(url, apiKey, properties.getNotifyUrl());
}); });
@ -347,7 +359,7 @@ public class AiModelFactoryImpl implements AiModelFactory {
* 可参考 {@link DashScopeImageAutoConfiguration} dashScopeImageModel 方法 * 可参考 {@link DashScopeImageAutoConfiguration} dashScopeImageModel 方法
*/ */
private static DashScopeImageModel buildTongYiImagesModel(String key) { private static DashScopeImageModel buildTongYiImagesModel(String key) {
DashScopeImageApi dashScopeImageApi = new DashScopeImageApi(key); DashScopeImageApi dashScopeImageApi = DashScopeImageApi.builder().apiKey(key).build();
return DashScopeImageModel.builder() return DashScopeImageModel.builder()
.dashScopeApi(dashScopeImageApi) .dashScopeApi(dashScopeImageApi)
.build(); .build();
@ -397,7 +409,7 @@ public class AiModelFactoryImpl implements AiModelFactory {
* 可参考 {@link AiAutoConfiguration#douBaoChatClient(YudaoAiProperties)} * 可参考 {@link AiAutoConfiguration#douBaoChatClient(YudaoAiProperties)}
*/ */
private ChatModel buildDouBaoChatModel(String apiKey) { private ChatModel buildDouBaoChatModel(String apiKey) {
YudaoAiProperties.DouBaoProperties properties = new YudaoAiProperties.DouBaoProperties() YudaoAiProperties.DouBao properties = new YudaoAiProperties.DouBao()
.setApiKey(apiKey); .setApiKey(apiKey);
return new AiAutoConfiguration().buildDouBaoChatClient(properties); return new AiAutoConfiguration().buildDouBaoChatClient(properties);
} }
@ -406,7 +418,7 @@ public class AiModelFactoryImpl implements AiModelFactory {
* 可参考 {@link AiAutoConfiguration#hunYuanChatClient(YudaoAiProperties)} * 可参考 {@link AiAutoConfiguration#hunYuanChatClient(YudaoAiProperties)}
*/ */
private ChatModel buildHunYuanChatModel(String apiKey, String url) { private ChatModel buildHunYuanChatModel(String apiKey, String url) {
YudaoAiProperties.HunYuanProperties properties = new YudaoAiProperties.HunYuanProperties() YudaoAiProperties.HunYuan properties = new YudaoAiProperties.HunYuan()
.setBaseUrl(url).setApiKey(apiKey); .setBaseUrl(url).setApiKey(apiKey);
return new AiAutoConfiguration().buildHunYuanChatClient(properties); return new AiAutoConfiguration().buildHunYuanChatClient(properties);
} }
@ -415,7 +427,7 @@ public class AiModelFactoryImpl implements AiModelFactory {
* 可参考 {@link AiAutoConfiguration#siliconFlowChatClient(YudaoAiProperties)} * 可参考 {@link AiAutoConfiguration#siliconFlowChatClient(YudaoAiProperties)}
*/ */
private ChatModel buildSiliconFlowChatModel(String apiKey) { private ChatModel buildSiliconFlowChatModel(String apiKey) {
YudaoAiProperties.SiliconFlowProperties properties = new YudaoAiProperties.SiliconFlowProperties() YudaoAiProperties.SiliconFlow properties = new YudaoAiProperties.SiliconFlow()
.setApiKey(apiKey); .setApiKey(apiKey);
return new AiAutoConfiguration().buildSiliconFlowChatClient(properties); return new AiAutoConfiguration().buildSiliconFlowChatClient(properties);
} }
@ -473,7 +485,7 @@ public class AiModelFactoryImpl implements AiModelFactory {
private static XingHuoChatModel buildXingHuoChatModel(String key) { private static XingHuoChatModel buildXingHuoChatModel(String key) {
List<String> keys = StrUtil.split(key, '|'); List<String> keys = StrUtil.split(key, '|');
Assert.equals(keys.size(), 2, "XingHuoChatClient 的密钥需要 (appKey|secretKey) 格式"); Assert.equals(keys.size(), 2, "XingHuoChatClient 的密钥需要 (appKey|secretKey) 格式");
YudaoAiProperties.XingHuoProperties properties = new YudaoAiProperties.XingHuoProperties() YudaoAiProperties.XingHuo properties = new YudaoAiProperties.XingHuo()
.setAppKey(keys.get(0)).setSecretKey(keys.get(1)); .setAppKey(keys.get(0)).setSecretKey(keys.get(1));
return new AiAutoConfiguration().buildXingHuoChatClient(properties); return new AiAutoConfiguration().buildXingHuoChatClient(properties);
} }
@ -482,7 +494,7 @@ public class AiModelFactoryImpl implements AiModelFactory {
* 可参考 {@link AiAutoConfiguration#baiChuanChatClient(YudaoAiProperties)} * 可参考 {@link AiAutoConfiguration#baiChuanChatClient(YudaoAiProperties)}
*/ */
private BaiChuanChatModel buildBaiChuanChatModel(String apiKey) { private BaiChuanChatModel buildBaiChuanChatModel(String apiKey) {
YudaoAiProperties.BaiChuanProperties properties = new YudaoAiProperties.BaiChuanProperties() YudaoAiProperties.BaiChuan properties = new YudaoAiProperties.BaiChuan()
.setApiKey(apiKey); .setApiKey(apiKey);
return new AiAutoConfiguration().buildBaiChuanChatClient(properties); return new AiAutoConfiguration().buildBaiChuanChatClient(properties);
} }
@ -512,6 +524,30 @@ public class AiModelFactoryImpl implements AiModelFactory {
.build(); .build();
} }
/**
* 可参考 {@link AnthropicChatAutoConfiguration} anthropicApi 方法
*/
private static AnthropicChatModel buildAnthropicChatModel(String apiKey, String url) {
AnthropicApi.Builder builder = AnthropicApi.builder().apiKey(apiKey);
if (StrUtil.isNotEmpty(url)) {
builder.baseUrl(url);
}
AnthropicApi anthropicApi = builder.build();
return AnthropicChatModel.builder()
.anthropicApi(anthropicApi)
.toolCallingManager(getToolCallingManager())
.build();
}
/**
* 可参考 {@link AiAutoConfiguration#buildGeminiChatClient(YudaoAiProperties.Gemini)}
*/
private static GeminiChatModel buildGeminiChatModel(String apiKey) {
YudaoAiProperties.Gemini properties = SpringUtil.getBean(YudaoAiProperties.class)
.getGemini().setApiKey(apiKey);
return new AiAutoConfiguration().buildGeminiChatClient(properties);
}
/** /**
* 可参考 {@link OpenAiImageAutoConfiguration} openAiImageModel 方法 * 可参考 {@link OpenAiImageAutoConfiguration} openAiImageModel 方法
*/ */

View File

@ -0,0 +1,174 @@
# AiBoChaWebSearchClient 使用指南
## 概述
`AiBoChaWebSearchClient` 是基于博查AI开放平台提供的网页搜索服务的Java客户端实现了符合项目架构风格的HTTP客户端封装。
## 特性
- **统一的API调用风格**:参考 SunoApi 和 XunFeiPptApi 的实现方式
- **Record 类型数据结构**:使用 Record 类型定义请求和响应数据
- **简洁的响应数据模型**:包含网页搜索结果
- **灵活的搜索配置**:支持时间范围、域名过滤、结果数量等参数
- **错误处理机制**:统一的异常处理和日志记录
## 快速开始
### 1. 创建客户端实例
```java
// 使用默认base URL
AiBoChaWebSearchClient client = new AiBoChaWebSearchClient("your-api-key");
// 使用自定义base URL
AiBoChaWebSearchClient client = new AiBoChaWebSearchClient("https://custom.api.com", "your-api-key");
```
### 2. 基本搜索
```java
// 基本搜索
WebSearchRequest request = new WebSearchRequest(
"Spring Boot 教程",
null, null, null, null, null
);
AiWebSearchResponse result = client.search(request);
```
### 3. 高级搜索
```java
// 构建详细的搜索请求
WebSearchRequest request = new WebSearchRequest(
"人工智能最新进展",
FreshnessType.ONE_WEEK.getValue(), // 搜索一周内的内容
true, // 显示摘要
"zhihu.com|csdn.net", // 只搜索指定域名
"spam.com", // 排除指定域名
20 // 返回20条结果
);
AiWebSearchResponse result = client.search(request);
```
## API参数说明
### 请求参数
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| query | String | 是 | 用户的搜索词 |
| freshness | String | 否 | 搜索时间范围,默认为 noLimit |
| summary | Boolean | 否 | 是否显示文本摘要,默认为 false |
| include | String | 否 | 指定搜索的网站范围,多个域名使用\|或,分隔 |
| exclude | String | 否 | 排除搜索的网站范围,多个域名使用\|或,分隔 |
| count | Integer | 否 | 返回结果条数范围1-50默认为10 |
### 时间范围选项
使用 `FreshnessType` 枚举:
```java
FreshnessType.NO_LIMIT // 不限(默认)
FreshnessType.ONE_DAY // 一天内
FreshnessType.ONE_WEEK // 一周内
FreshnessType.ONE_MONTH // 一个月内
FreshnessType.ONE_YEAR // 一年内
```
也可以使用自定义日期范围:
- 日期范围:`"2025-01-01..2025-04-06"`
- 指定日期:`"2025-04-06"`
### 响应数据结构
```java
// 主要响应数据
AiWebSearchResponse result = client.search(request);
// 网页搜索结果
List<AiWebSearchResponse.WebPage> webPages = result.webPages();
for (AiWebSearchResponse.WebPage page : webPages) {
String title = page.title(); // 网页标题
String url = page.url(); // 网页URL
String snippet = page.snippet(); // 内容描述
String summary = page.summary(); // 文本摘要如果请求了summary
String siteName = page.siteName(); // 网站名称
}
```
## 使用示例
### 示例1搜索技术文档
```java
WebSearchRequest request = new WebSearchRequest(
"Spring Boot 3.x 新特性",
FreshnessType.ONE_MONTH.getValue(),
true,
"spring.io|baeldung.com|github.com",
null,
15
);
AiWebSearchResponse result = client.search(request);
```
### 示例2搜索新闻资讯
```java
WebSearchRequest request = new WebSearchRequest(
"AI大模型发展趋势",
FreshnessType.ONE_WEEK.getValue(),
null,
null,
"advertisement.com|spam.net",
30
);
AiWebSearchResponse result = client.search(request);
```
## 注意事项
1. **API密钥**需要先到博查AI开放平台https://open.bochaai.com获取API KEY
2. **请求频率**注意遵守平台的API调用频率限制
3. **时间范围**:建议使用 `noLimit` 以获得更好的搜索效果
4. **域名过滤**include和exclude参数最多支持20个域名
5. **结果数量**单次搜索最多返回50条结果
## 集成建议
在Spring Boot项目中建议将客户端配置为Bean
```java
@Configuration
public class AiConfiguration {
@Value("${ai.bocha.api-key}")
private String apiKey;
@Value("${ai.bocha.base-url:https://open.bochaai.com}")
private String baseUrl;
@Bean
public AiBoChaWebSearchClient boChaWebSearchClient() {
return new AiBoChaWebSearchClient(baseUrl, apiKey);
}
}
```
## 故障排查
1. **网络连接问题**:检查网络连接和防火墙设置
2. **API密钥错误**确认API KEY正确且有效
3. **请求参数错误**:检查必填参数是否正确填写
4. **服务器响应错误**:查看日志中的详细错误信息
## 更新日志
- v2.0.0:重大重构,统一 Record 类型,简化 API 调用,支持新的响应结构
- v1.3.0:统一使用 Record 类型,移除 Lombok 注解,保持代码风格一致性
- v1.2.0:进一步简化,移除视频搜索功能,专注于网页搜索
- v1.1.0:使用 Lombok 简化代码,移除图片搜索功能
- v1.0.0:初始版本,实现基本的网页搜索功能

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.model.ChatResponse;
import org.springframework.ai.chat.prompt.ChatOptions; import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.ai.chat.prompt.Prompt; import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.openai.OpenAiChatModel;
import reactor.core.publisher.Flux; import reactor.core.publisher.Flux;
/** /**
@ -19,13 +18,14 @@ import reactor.core.publisher.Flux;
public class DouBaoChatModel implements ChatModel { public class DouBaoChatModel implements ChatModel {
public static final String BASE_URL = "https://ark.cn-beijing.volces.com/api"; 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-1-5-lite-32k-250115";
/** /**
* 兼容 OpenAI 接口进行复用 * 兼容 OpenAI 接口进行复用
*/ */
private final OpenAiChatModel openAiChatModel; private final ChatModel openAiChatModel;
@Override @Override
public ChatResponse call(Prompt prompt) { public ChatResponse call(Prompt prompt) {

View File

@ -0,0 +1,46 @@
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

@ -6,7 +6,6 @@ import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.ChatOptions; import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.ai.chat.prompt.Prompt; import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.openai.OpenAiChatModel;
import reactor.core.publisher.Flux; import reactor.core.publisher.Flux;
/** /**
@ -22,6 +21,7 @@ import reactor.core.publisher.Flux;
public class HunYuanChatModel implements ChatModel { public class HunYuanChatModel implements ChatModel {
public static final String BASE_URL = "https://api.hunyuan.cloud.tencent.com"; public static final String BASE_URL = "https://api.hunyuan.cloud.tencent.com";
public static final String COMPLETE_PATH = "/v1/chat/completions";
public static final String MODEL_DEFAULT = "hunyuan-turbo"; public static final String MODEL_DEFAULT = "hunyuan-turbo";
@ -32,7 +32,7 @@ public class HunYuanChatModel implements ChatModel {
/** /**
* 兼容 OpenAI 接口进行复用 * 兼容 OpenAI 接口进行复用
*/ */
private final OpenAiChatModel openAiChatModel; private final ChatModel openAiChatModel;
@Override @Override
public ChatResponse call(Prompt prompt) { public ChatResponse call(Prompt prompt) {

View File

@ -23,7 +23,7 @@ public class SiliconFlowChatModel implements ChatModel {
/** /**
* 兼容 OpenAI 接口进行复用 * 兼容 OpenAI 接口进行复用
*/ */
private final OpenAiChatModel openAiChatModel; private final ChatModel openAiChatModel;
@Override @Override
public ChatResponse call(Prompt prompt) { public ChatResponse call(Prompt prompt) {

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.model.ChatResponse;
import org.springframework.ai.chat.prompt.ChatOptions; import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.ai.chat.prompt.Prompt; import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.openai.OpenAiChatModel;
import reactor.core.publisher.Flux; import reactor.core.publisher.Flux;
/** /**
@ -18,28 +17,34 @@ import reactor.core.publisher.Flux;
@RequiredArgsConstructor @RequiredArgsConstructor
public class XingHuoChatModel implements ChatModel { public class XingHuoChatModel implements ChatModel {
public static final String BASE_URL = "https://spark-api-open.xf-yun.com"; public static final String BASE_URL_V1 = "https://spark-api-open.xf-yun.com";
public static final String MODEL_DEFAULT = "generalv3.5"; 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";
/** /**
* 兼容 OpenAI 接口进行复用 * 已知模型名列表x14.0Ultrageneralv3.5max-32kgeneralv3pro-128klite
*/ */
private final OpenAiChatModel openAiChatModel; public static final String MODEL_DEFAULT = "4.0Ultra";
/**
* v1 兼容 OpenAI 接口进行复用
*/
private final ChatModel openAiChatModelV1;
@Override @Override
public ChatResponse call(Prompt prompt) { public ChatResponse call(Prompt prompt) {
return openAiChatModel.call(prompt); return openAiChatModelV1.call(prompt);
} }
@Override @Override
public Flux<ChatResponse> stream(Prompt prompt) { public Flux<ChatResponse> stream(Prompt prompt) {
return openAiChatModel.stream(prompt); return openAiChatModelV1.stream(prompt);
} }
@Override @Override
public ChatOptions getDefaultOptions() { public ChatOptions getDefaultOptions() {
return openAiChatModel.getDefaultOptions(); return openAiChatModelV1.getDefaultOptions();
} }
} }

View File

@ -0,0 +1,18 @@
package cn.iocoder.yudao.module.ai.framework.ai.core.webserch;
/**
* 网络搜索客户端接口
*
* @author 芋道源码
*/
public interface AiWebSearchClient {
/**
* 网页搜索
*
* @param request 搜索请求
* @return 搜索结果
*/
AiWebSearchResponse search(AiWebSearchRequest request);
}

View File

@ -0,0 +1,34 @@
package cn.iocoder.yudao.module.ai.framework.ai.core.webserch;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
@Data
public class AiWebSearchRequest {
/**
* 用户的搜索词
*/
@NotEmpty(message = "搜索词不能为空")
private String query;
/**
* 是否显示文本摘要
*
* true - 显示
* false - 不显示默认
*/
private Boolean summary;
/**
* 返回结果的条数
*/
@NotNull(message = "返回结果条数不能为空")
@Min(message = "返回结果条数最小为 1", value = 1)
@Max(message = "返回结果条数最大为 50", value = 50)
private Integer count;
}

View File

@ -0,0 +1,62 @@
package cn.iocoder.yudao.module.ai.framework.ai.core.webserch;
import lombok.Data;
import java.util.List;
@Data
public class AiWebSearchResponse {
/**
* 总数总共匹配的网页数
*/
private Long total;
/**
* 数据列表
*/
private List<WebPage> lists;
/**
* 网页对象
*/
@Data
public static class WebPage {
/**
* 名称
*
* 例如说搜狐网
*/
private String name;
/**
* 图标
*/
private String icon;
/**
* 标题
*
* 例如说186页|阿里巴巴2024年环境社会和治理ESG报告
*/
private String title;
/**
* URL
*
* 例如说https://m.sohu.com/a/815036254_121819701/?pvid=000115_3w_a
*/
@SuppressWarnings("JavadocLinkAsPlainText")
private String url;
/**
* 内容的简短描述
*/
private String snippet;
/**
* 内容的文本摘要
*/
private String summary;
}
}

View File

@ -0,0 +1,153 @@
package cn.iocoder.yudao.module.ai.framework.ai.core.webserch.bocha;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.Assert;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.AiWebSearchClient;
import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.AiWebSearchRequest;
import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.AiWebSearchResponse;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.client.ClientResponse;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;
import java.util.List;
import java.util.function.Function;
import java.util.function.Predicate;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
/**
* 博查 {@link AiWebSearchClient} 实现类
*
* @see <a href="https://open.bochaai.com/overview">博查 AI 开放平台</a>
*
* @author 芋道源码
*/
@Slf4j
public class AiBoChaWebSearchClient implements AiWebSearchClient {
public static final String BASE_URL = "https://api.bochaai.com";
private static final String AUTHORIZATION_HEADER = "Authorization";
private static final String BEARER_PREFIX = "Bearer ";
private final WebClient webClient;
private final Predicate<HttpStatusCode> STATUS_PREDICATE = status -> !status.is2xxSuccessful();
private final Function<Object, Function<ClientResponse, Mono<? extends Throwable>>> EXCEPTION_FUNCTION =
reqParam -> response -> response.bodyToMono(String.class).handle((responseBody, sink) -> {
log.error("[AiBoChaWebSearchClient] 调用失败!请求参数:[{}],响应数据: [{}]", reqParam, responseBody);
sink.error(new IllegalStateException("[AiBoChaWebSearchClient] 调用失败!"));
});
public AiBoChaWebSearchClient(String apiKey) {
this.webClient = WebClient.builder()
.baseUrl(BASE_URL)
.defaultHeaders((headers) -> {
headers.setContentType(MediaType.APPLICATION_JSON);
headers.add(AUTHORIZATION_HEADER, BEARER_PREFIX + apiKey);
})
.build();
}
@Override
public AiWebSearchResponse search(AiWebSearchRequest request) {
// 转换请求参数
WebSearchRequest webSearchRequest = new WebSearchRequest(
request.getQuery(),
request.getSummary(),
request.getCount()
);
// 调用博查 API
CommonResult<WebSearchResponse> response = this.webClient.post()
.uri("/v1/web-search")
.bodyValue(webSearchRequest)
.retrieve()
.onStatus(STATUS_PREDICATE, EXCEPTION_FUNCTION.apply(webSearchRequest))
.bodyToMono(new ParameterizedTypeReference<CommonResult<WebSearchResponse>>() {})
.block();
if (response == null) {
throw new IllegalStateException("[search][搜索结果为空]");
}
if (response.getData() == null) {
throw new IllegalStateException(String.format("[search][搜索失败code = %s, msg = %s]",
response.getCode(), response.getMsg()));
}
WebSearchResponse data = response.getData();
// 转换结果
AiWebSearchResponse result = new AiWebSearchResponse();
if (data.webPages() == null || CollUtil.isEmpty(data.webPages().value())) {
return result.setTotal(0L).setLists(List.of());
}
return result.setTotal(data.webPages().totalEstimatedMatches())
.setLists(convertList(data.webPages().value(), page -> new AiWebSearchResponse.WebPage()
.setName(page.siteName()).setIcon(page.siteIcon())
.setTitle(page.name()).setUrl(page.url())
.setSnippet(page.snippet()).setSummary(page.summary())));
}
/**
* 网页搜索请求参数
*/
@JsonInclude(value = JsonInclude.Include.NON_NULL)
public record WebSearchRequest(
String query,
Boolean summary,
Integer count
) {
public WebSearchRequest {
Assert.notBlank(query, "query 不能为空");
}
}
/**
* 网页搜索响应
*/
@JsonInclude(value = JsonInclude.Include.NON_NULL)
public record WebSearchResponse(
WebSearchWebPages webPages
) {
}
/**
* 网页搜索结果
*/
@JsonInclude(value = JsonInclude.Include.NON_NULL)
public record WebSearchWebPages(
String webSearchUrl,
Long totalEstimatedMatches,
List<WebPageValue> value,
Boolean someResultsRemoved
) {
/**
* 网页结果值
*/
@JsonInclude(value = JsonInclude.Include.NON_NULL)
public record WebPageValue(
String id,
String name,
String url,
String displayUrl,
String snippet,
String summary,
String siteName,
String siteIcon,
String datePublished,
String dateLastCrawled,
String cachedPageUrl,
String language,
Boolean isFamilyFriendly,
Boolean isNavigational
) {
}
}
}

View File

@ -2,6 +2,8 @@ package cn.iocoder.yudao.module.ai.framework.security.config;
import cn.iocoder.yudao.framework.security.config.AuthorizeRequestsCustomizer; import cn.iocoder.yudao.framework.security.config.AuthorizeRequestsCustomizer;
import cn.iocoder.yudao.module.infra.enums.ApiConstants; import cn.iocoder.yudao.module.infra.enums.ApiConstants;
import jakarta.annotation.Resource;
import org.springframework.ai.mcp.server.autoconfigure.McpServerProperties;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity;
@ -13,6 +15,9 @@ import org.springframework.security.config.annotation.web.configurers.AuthorizeH
@Configuration(proxyBeanMethods = false, value = "aiSecurityConfiguration") @Configuration(proxyBeanMethods = false, value = "aiSecurityConfiguration")
public class SecurityConfiguration { public class SecurityConfiguration {
@Resource
private McpServerProperties serverProperties;
@Bean("aiAuthorizeRequestsCustomizer") @Bean("aiAuthorizeRequestsCustomizer")
public AuthorizeRequestsCustomizer authorizeRequestsCustomizer() { public AuthorizeRequestsCustomizer authorizeRequestsCustomizer() {
return new AuthorizeRequestsCustomizer() { return new AuthorizeRequestsCustomizer() {
@ -33,6 +38,10 @@ public class SecurityConfiguration {
// TODO 芋艿这个每个项目都需要重复配置得捉摸有没通用的方案 // TODO 芋艿这个每个项目都需要重复配置得捉摸有没通用的方案
// RPC 服务的安全配置 // RPC 服务的安全配置
registry.requestMatchers(ApiConstants.PREFIX + "/**").permitAll(); registry.requestMatchers(ApiConstants.PREFIX + "/**").permitAll();
// MCP Server
registry.requestMatchers(serverProperties.getSseEndpoint()).permitAll();
registry.requestMatchers(serverProperties.getSseMessageEndpoint()).permitAll();
} }
}; };

View File

@ -1,10 +1,11 @@
package cn.iocoder.yudao.module.ai.service.chat; package cn.iocoder.yudao.module.ai.service.chat;
import cn.hutool.core.codec.Base64;
import cn.hutool.core.collection.CollUtil; import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.io.file.FileNameUtil;
import cn.hutool.core.util.ObjUtil; import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.module.ai.enums.model.AiPlatformEnum; import cn.hutool.http.HttpUtil;
import cn.iocoder.yudao.module.ai.util.AiUtils;
import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
@ -21,6 +22,10 @@ import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiModelDO;
import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiToolDO; import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiToolDO;
import cn.iocoder.yudao.module.ai.dal.mysql.chat.AiChatMessageMapper; import cn.iocoder.yudao.module.ai.dal.mysql.chat.AiChatMessageMapper;
import cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants; import cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants;
import cn.iocoder.yudao.module.ai.enums.model.AiPlatformEnum;
import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.AiWebSearchClient;
import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.AiWebSearchRequest;
import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.AiWebSearchResponse;
import cn.iocoder.yudao.module.ai.service.knowledge.AiKnowledgeDocumentService; import cn.iocoder.yudao.module.ai.service.knowledge.AiKnowledgeDocumentService;
import cn.iocoder.yudao.module.ai.service.knowledge.AiKnowledgeSegmentService; import cn.iocoder.yudao.module.ai.service.knowledge.AiKnowledgeSegmentService;
import cn.iocoder.yudao.module.ai.service.knowledge.bo.AiKnowledgeSegmentSearchReqBO; import cn.iocoder.yudao.module.ai.service.knowledge.bo.AiKnowledgeSegmentSearchReqBO;
@ -28,7 +33,11 @@ import cn.iocoder.yudao.module.ai.service.knowledge.bo.AiKnowledgeSegmentSearchR
import cn.iocoder.yudao.module.ai.service.model.AiChatRoleService; import cn.iocoder.yudao.module.ai.service.model.AiChatRoleService;
import cn.iocoder.yudao.module.ai.service.model.AiModelService; import cn.iocoder.yudao.module.ai.service.model.AiModelService;
import cn.iocoder.yudao.module.ai.service.model.AiToolService; import cn.iocoder.yudao.module.ai.service.model.AiToolService;
import javax.annotation.Resource; import cn.iocoder.yudao.module.ai.util.AiUtils;
import cn.iocoder.yudao.module.ai.util.FileTypeUtils;
import com.google.common.collect.Maps;
import io.modelcontextprotocol.client.McpSyncClient;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.messages.Message; import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.MessageType; import org.springframework.ai.chat.messages.MessageType;
@ -39,6 +48,11 @@ import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.model.StreamingChatModel; import org.springframework.ai.chat.model.StreamingChatModel;
import org.springframework.ai.chat.prompt.ChatOptions; import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.ai.chat.prompt.Prompt; import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.mcp.SyncMcpToolCallbackProvider;
import org.springframework.ai.mcp.client.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.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import reactor.core.publisher.Flux; import reactor.core.publisher.Flux;
@ -64,6 +78,13 @@ import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.CHAT_MESSAGE_N
@Slf4j @Slf4j
public class AiChatMessageServiceImpl implements AiChatMessageService { public class AiChatMessageServiceImpl implements AiChatMessageService {
/**
* 联网搜索的结束数
*/
private static final Integer WEB_SEARCH_COUNT = 10;
// TODO @芋艿后续优化下对话的 Prompt 整体结构
/** /**
* 知识库转 {@link UserMessage} 的内容模版 * 知识库转 {@link UserMessage} 的内容模版
*/ */
@ -71,6 +92,18 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
"%s\n\n" + // 多个 <Reference></Reference> 的拼接 "%s\n\n" + // 多个 <Reference></Reference> 的拼接
"回答要求:\n- 避免提及你是从 <Reference></Reference> 获取的知识。"; "回答要求:\n- 避免提及你是从 <Reference></Reference> 获取的知识。";
private static final String WEB_SEARCH_USER_MESSAGE_TEMPLATE = "使用 <WebSearch></WebSearch> 标记中的内容作为本次对话的参考:\n\n" +
"%s\n\n" + // 多个 <WebSearch></WebSearch> 的拼接
"回答要求:\n- 避免提及你是从 <WebSearch></WebSearch> 获取的知识。";
/**
* 附件转 ${@link UserMessage} 的内容模版
*/
@SuppressWarnings("TextBlockMigration")
private static final String Attachment_USER_MESSAGE_TEMPLATE = "使用 <Attachment></Attachment> 标记用户对话上传的附件内容:\n\n" +
"%s\n\n" + // 多个 <Attachment></Attachment> 的拼接
"回答要求:\n- 避免提及 <Attachment></Attachment> 附件的编码格式。";
@Resource @Resource
private AiChatMessageMapper chatMessageMapper; private AiChatMessageMapper chatMessageMapper;
@ -87,6 +120,21 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
@Resource @Resource
private AiToolService toolService; private AiToolService toolService;
@SuppressWarnings("SpringJavaAutowiredFieldsWarningInspection")
@Autowired(required = false) // 由于 yudao.ai.web-search.enable 配置项可以关闭 AiWebSearchClient 的功能所以这里只能不强制注入
private AiWebSearchClient webSearchClient;
@SuppressWarnings("SpringJavaAutowiredFieldsWarningInspection")
@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;
@Resource
private ToolCallbackResolver toolCallbackResolver;
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public AiChatMessageSendRespVO sendMessage(AiChatMessageSendReqVO sendReqVO, Long userId) { public AiChatMessageSendRespVO sendMessage(AiChatMessageSendReqVO sendReqVO, Long userId) {
// 1.1 校验对话存在 // 1.1 校验对话存在
@ -100,27 +148,35 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
AiModelDO model = modalService.validateModel(conversation.getModelId()); AiModelDO model = modalService.validateModel(conversation.getModelId());
ChatModel chatModel = modalService.getChatModel(model.getId()); ChatModel chatModel = modalService.getChatModel(model.getId());
// 2. 知识库找回 // 2.1 知识库召回
List<AiKnowledgeSegmentSearchRespBO> knowledgeSegments = recallKnowledgeSegment(sendReqVO.getContent(), conversation); List<AiKnowledgeSegmentSearchRespBO> knowledgeSegments = recallKnowledgeSegment(
sendReqVO.getContent(), conversation);
// 2.2 联网搜索
AiWebSearchResponse webSearchResponse = Boolean.TRUE.equals(sendReqVO.getUseSearch()) && webSearchClient != null ?
webSearchClient.search(new AiWebSearchRequest().setQuery(sendReqVO.getContent())
.setSummary(true).setCount(WEB_SEARCH_COUNT)) : null;
// 3. 插入 user 发送消息 // 3. 插入 user 发送消息
AiChatMessageDO userMessage = createChatMessage(conversation.getId(), null, model, AiChatMessageDO userMessage = createChatMessage(conversation.getId(), null, model,
userId, conversation.getRoleId(), MessageType.USER, sendReqVO.getContent(), sendReqVO.getUseContext(), userId, conversation.getRoleId(), MessageType.USER, sendReqVO.getContent(), sendReqVO.getUseContext(),
null); null, sendReqVO.getAttachmentUrls(), null);
// 3.1 插入 assistant 接收消息 // 4.1 插入 assistant 接收消息
AiChatMessageDO assistantMessage = createChatMessage(conversation.getId(), userMessage.getId(), model, AiChatMessageDO assistantMessage = createChatMessage(conversation.getId(), userMessage.getId(), model,
userId, conversation.getRoleId(), MessageType.ASSISTANT, "", sendReqVO.getUseContext(), userId, conversation.getRoleId(), MessageType.ASSISTANT, "", sendReqVO.getUseContext(),
knowledgeSegments); knowledgeSegments, null, webSearchResponse);
// 3.2 创建 chat 需要的 Prompt // 4.2 创建 chat 需要的 Prompt
Prompt prompt = buildPrompt(conversation, historyMessages, knowledgeSegments, model, sendReqVO); Prompt prompt = buildPrompt(conversation, historyMessages, knowledgeSegments, webSearchResponse, model, sendReqVO);
ChatResponse chatResponse = chatModel.call(prompt); ChatResponse chatResponse = chatModel.call(prompt);
// 3.3 更新响应内容 // 4.3 更新响应内容
String newContent = chatResponse.getResult().getOutput().getText(); String newContent = AiUtils.getChatResponseContent(chatResponse);
chatMessageMapper.updateById(new AiChatMessageDO().setId(assistantMessage.getId()).setContent(newContent)); String newReasoningContent = AiUtils.getChatResponseReasoningContent(chatResponse);
// 3.4 响应结果 chatMessageMapper.updateById(new AiChatMessageDO().setId(assistantMessage.getId())
.setContent(newContent).setReasoningContent(newReasoningContent));
// 4.4 响应结果
Map<Long, AiKnowledgeDocumentDO> documentMap = knowledgeDocumentService.getKnowledgeDocumentMap( Map<Long, AiKnowledgeDocumentDO> documentMap = knowledgeDocumentService.getKnowledgeDocumentMap(
convertSet(knowledgeSegments, AiKnowledgeSegmentSearchRespBO::getDocumentId)); convertSet(knowledgeSegments, AiKnowledgeSegmentSearchRespBO::getDocumentId));
List<AiChatMessageRespVO.KnowledgeSegment> segments = BeanUtils.toBean(knowledgeSegments, List<AiChatMessageRespVO.KnowledgeSegment> segments = BeanUtils.toBean(knowledgeSegments,
@ -131,7 +187,8 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
return new AiChatMessageSendRespVO() return new AiChatMessageSendRespVO()
.setSend(BeanUtils.toBean(userMessage, AiChatMessageSendRespVO.Message.class)) .setSend(BeanUtils.toBean(userMessage, AiChatMessageSendRespVO.Message.class))
.setReceive(BeanUtils.toBean(assistantMessage, AiChatMessageSendRespVO.Message.class) .setReceive(BeanUtils.toBean(assistantMessage, AiChatMessageSendRespVO.Message.class)
.setContent(newContent).setSegments(segments)); .setContent(newContent).setSegments(segments)
.setWebSearchPages(webSearchResponse != null ? webSearchResponse.getLists() : null));
} }
@Override @Override
@ -148,29 +205,36 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
AiModelDO model = modalService.validateModel(conversation.getModelId()); AiModelDO model = modalService.validateModel(conversation.getModelId());
StreamingChatModel chatModel = modalService.getChatModel(model.getId()); StreamingChatModel chatModel = modalService.getChatModel(model.getId());
// 2. 知识库找回 // 2.1 知识库找回
List<AiKnowledgeSegmentSearchRespBO> knowledgeSegments = recallKnowledgeSegment(sendReqVO.getContent(), List<AiKnowledgeSegmentSearchRespBO> knowledgeSegments = recallKnowledgeSegment(
conversation); sendReqVO.getContent(), conversation);
// 2.2 联网搜索
AiWebSearchResponse webSearchResponse = Boolean.TRUE.equals(sendReqVO.getUseSearch()) && webSearchClient != null ?
webSearchClient.search(new AiWebSearchRequest().setQuery(sendReqVO.getContent())
.setSummary(true).setCount(WEB_SEARCH_COUNT)) : null;
// 3. 插入 user 发送消息 // 3. 插入 user 发送消息
AiChatMessageDO userMessage = createChatMessage(conversation.getId(), null, model, AiChatMessageDO userMessage = createChatMessage(conversation.getId(), null, model,
userId, conversation.getRoleId(), MessageType.USER, sendReqVO.getContent(), sendReqVO.getUseContext(), userId, conversation.getRoleId(), MessageType.USER, sendReqVO.getContent(), sendReqVO.getUseContext(),
null); null, sendReqVO.getAttachmentUrls(), null);
// 4.1 插入 assistant 接收消息 // 4.1 插入 assistant 接收消息
AiChatMessageDO assistantMessage = createChatMessage(conversation.getId(), userMessage.getId(), model, AiChatMessageDO assistantMessage = createChatMessage(conversation.getId(), userMessage.getId(), model,
userId, conversation.getRoleId(), MessageType.ASSISTANT, "", sendReqVO.getUseContext(), userId, conversation.getRoleId(), MessageType.ASSISTANT, "", sendReqVO.getUseContext(),
knowledgeSegments); knowledgeSegments, null, webSearchResponse);
// 4.2 构建 Prompt并进行调用 // 4.2 构建 Prompt并进行调用
Prompt prompt = buildPrompt(conversation, historyMessages, knowledgeSegments, model, sendReqVO); Prompt prompt = buildPrompt(conversation, historyMessages, knowledgeSegments, webSearchResponse, model, sendReqVO);
Flux<ChatResponse> streamResponse = chatModel.stream(prompt); Flux<ChatResponse> streamResponse = chatModel.stream(prompt);
// 4.3 流式返回 // 4.3 流式返回
StringBuffer contentBuffer = new StringBuffer(); StringBuffer contentBuffer = new StringBuffer();
StringBuffer reasoningContentBuffer = new StringBuffer();
return streamResponse.map(chunk -> { return streamResponse.map(chunk -> {
// 处理知识库的返回只有首次才有 // 仅首次返回知识库联网搜索
List<AiChatMessageRespVO.KnowledgeSegment> segments = null; List<AiChatMessageRespVO.KnowledgeSegment> segments = null;
List<AiWebSearchResponse.WebPage> webSearchPages = null;
if (StrUtil.isEmpty(contentBuffer)) { if (StrUtil.isEmpty(contentBuffer)) {
Map<Long, AiKnowledgeDocumentDO> documentMap = TenantUtils.executeIgnore(() -> Map<Long, AiKnowledgeDocumentDO> documentMap = TenantUtils.executeIgnore(() ->
knowledgeDocumentService.getKnowledgeDocumentMap( knowledgeDocumentService.getKnowledgeDocumentMap(
@ -179,24 +243,56 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
AiKnowledgeDocumentDO document = documentMap.get(segment.getDocumentId()); AiKnowledgeDocumentDO document = documentMap.get(segment.getDocumentId());
segment.setDocumentName(document != null ? document.getName() : null); segment.setDocumentName(document != null ? document.getName() : null);
}); });
if (webSearchResponse != null) {
webSearchPages = webSearchResponse.getLists();
}
} }
// 响应结果 // 响应结果
String newContent = chunk.getResult() != null ? chunk.getResult().getOutput().getText() : null; String newContent = AiUtils.getChatResponseContent(chunk);
newContent = StrUtil.nullToDefault(newContent, ""); // 避免 null 情况 String newReasoningContent = AiUtils.getChatResponseReasoningContent(chunk);
contentBuffer.append(newContent); if (StrUtil.isNotEmpty(newContent)) {
contentBuffer.append(newContent);
}
if (StrUtil.isNotEmpty(newReasoningContent)) {
reasoningContentBuffer.append(newReasoningContent);
}
return success(new AiChatMessageSendRespVO() return success(new AiChatMessageSendRespVO()
.setSend(BeanUtils.toBean(userMessage, AiChatMessageSendRespVO.Message.class)) .setSend(BeanUtils.toBean(userMessage, AiChatMessageSendRespVO.Message.class))
.setReceive(BeanUtils.toBean(assistantMessage, AiChatMessageSendRespVO.Message.class) .setReceive(BeanUtils.toBean(assistantMessage, AiChatMessageSendRespVO.Message.class)
.setContent(newContent).setSegments(segments))); .setContent(StrUtil.nullToDefault(newContent, "")) // 避免 null 情况
.setReasoningContent(StrUtil.nullToDefault(newReasoningContent, "")) // 避免 null 情况
.setSegments(segments).setWebSearchPages(webSearchPages))); // 知识库 + 联网搜索
}).doOnComplete(() -> { }).doOnComplete(() -> {
// 忽略租户因为 Flux 异步无法透传租户 // 忽略租户因为 Flux 异步无法透传租户
TenantUtils.executeIgnore(() -> chatMessageMapper.updateById( TenantUtils.executeIgnore(() -> chatMessageMapper.updateById(
new AiChatMessageDO().setId(assistantMessage.getId()).setContent(contentBuffer.toString()))); new AiChatMessageDO().setId(assistantMessage.getId()).setContent(contentBuffer.toString())
.setReasoningContent(reasoningContentBuffer.toString())));
}).doOnError(throwable -> { }).doOnError(throwable -> {
log.error("[sendChatMessageStream][userId({}) sendReqVO({}) 发生异常]", userId, sendReqVO, throwable); log.error("[sendChatMessageStream][userId({}) sendReqVO({}) 发生异常]", userId, sendReqVO, throwable);
// 忽略租户因为 Flux 异步无法透传租户 // 忽略租户因为 Flux 异步无法透传租户
TenantUtils.executeIgnore(() -> chatMessageMapper.updateById( TenantUtils.executeIgnore(() -> {
new AiChatMessageDO().setId(assistantMessage.getId()).setContent(throwable.getMessage()))); // 如果有内容则更新内容
if (StrUtil.isNotEmpty(contentBuffer)) {
chatMessageMapper.updateById(new AiChatMessageDO().setId(assistantMessage.getId())
.setContent(contentBuffer.toString()).setReasoningContent(reasoningContentBuffer.toString()));
} else {
// 否则则进行删除
chatMessageMapper.deleteById(assistantMessage.getId());
}
});
}).doOnCancel(() -> {
log.info("[sendChatMessageStream][userId({}) sendReqVO({}) 取消请求]", userId, sendReqVO);
// 忽略租户因为 Flux 异步无法透传租户
TenantUtils.executeIgnore(() -> {
// 如果有内容则更新内容
if (StrUtil.isNotEmpty(contentBuffer)) {
chatMessageMapper.updateById(new AiChatMessageDO().setId(assistantMessage.getId())
.setContent(contentBuffer.toString()).setReasoningContent(reasoningContentBuffer.toString()));
} else {
// 否则则进行删除
chatMessageMapper.deleteById(assistantMessage.getId());
}
});
}).onErrorResume(error -> Flux.just(error(ErrorCodeConstants.CHAT_STREAM_ERROR))); }).onErrorResume(error -> Flux.just(error(ErrorCodeConstants.CHAT_STREAM_ERROR)));
} }
@ -211,7 +307,7 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
return Collections.emptyList(); return Collections.emptyList();
} }
// 2. 遍历 // 2. 遍历
List<AiKnowledgeSegmentSearchRespBO> knowledgeSegments = new ArrayList<>(); List<AiKnowledgeSegmentSearchRespBO> knowledgeSegments = new ArrayList<>();
for (Long knowledgeId : role.getKnowledgeIds()) { for (Long knowledgeId : role.getKnowledgeIds()) {
knowledgeSegments.addAll(knowledgeSegmentService.searchKnowledgeSegment(new AiKnowledgeSegmentSearchReqBO() knowledgeSegments.addAll(knowledgeSegmentService.searchKnowledgeSegment(new AiKnowledgeSegmentSearchReqBO()
@ -222,6 +318,7 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
private Prompt buildPrompt(AiChatConversationDO conversation, List<AiChatMessageDO> messages, private Prompt buildPrompt(AiChatConversationDO conversation, List<AiChatMessageDO> messages,
List<AiKnowledgeSegmentSearchRespBO> knowledgeSegments, List<AiKnowledgeSegmentSearchRespBO> knowledgeSegments,
AiWebSearchResponse webSearchResponse,
AiModelDO model, AiChatMessageSendReqVO sendReqVO) { AiModelDO model, AiChatMessageSendReqVO sendReqVO) {
List<Message> chatMessages = new ArrayList<>(); List<Message> chatMessages = new ArrayList<>();
// 1.1 System Context 角色设定 // 1.1 System Context 角色设定
@ -231,8 +328,14 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
// 1.2 历史 history message 历史消息 // 1.2 历史 history message 历史消息
List<AiChatMessageDO> contextMessages = filterContextMessages(messages, conversation, sendReqVO); List<AiChatMessageDO> contextMessages = filterContextMessages(messages, conversation, sendReqVO);
contextMessages contextMessages.forEach(message -> {
.forEach(message -> chatMessages.add(AiUtils.buildMessage(message.getType(), message.getContent()))); chatMessages.add(AiUtils.buildMessage(message.getType(), message.getContent()));
UserMessage attachmentUserMessage = buildAttachmentUserMessage(message.getAttachmentUrls());
if (attachmentUserMessage != null) {
chatMessages.add(attachmentUserMessage);
}
// TODO @芋艿历史的知识库历史的搜索要不要拼接
});
// 1.3 当前 user message 新发送消息 // 1.3 当前 user message 新发送消息
chatMessages.add(new UserMessage(sendReqVO.getContent())); chatMessages.add(new UserMessage(sendReqVO.getContent()));
@ -245,23 +348,76 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
chatMessages.add(new UserMessage(String.format(KNOWLEDGE_USER_MESSAGE_TEMPLATE, reference))); chatMessages.add(new UserMessage(String.format(KNOWLEDGE_USER_MESSAGE_TEMPLATE, reference)));
} }
// 2.1 查询 tool 工具 // 1.5 联网搜索通过 UserMessage 实现
Set<String> toolNames = null; if (webSearchResponse != null && CollUtil.isNotEmpty(webSearchResponse.getLists())) {
Map<String,Object> toolContext = Map.of(); String webSearch = webSearchResponse.getLists().stream()
if (conversation.getRoleId() != null) { .map(page -> {
AiChatRoleDO chatRole = chatRoleService.getChatRole(conversation.getRoleId()); String summary = StrUtil.isNotEmpty(page.getSummary()) ?
if (chatRole != null && CollUtil.isNotEmpty(chatRole.getToolIds())) { "\nSummary: " + page.getSummary() : "";
toolNames = convertSet(toolService.getToolList(chatRole.getToolIds()), AiToolDO::getName); return "<WebSearch title=\"" + page.getTitle() + "\" url=\"" + page.getUrl() + "\">"
toolContext = AiUtils.buildCommonToolContext(); + StrUtil.blankToDefault(page.getSummary(), page.getSnippet()) + "</WebSearch>";
})
.collect(Collectors.joining("\n\n"));
chatMessages.add(new UserMessage(String.format(WEB_SEARCH_USER_MESSAGE_TEMPLATE, webSearch)));
}
// 1.6 附件通过 UserMessage 实现
if (CollUtil.isNotEmpty(sendReqVO.getAttachmentUrls())) {
UserMessage attachmentUserMessage = buildAttachmentUserMessage(sendReqVO.getAttachmentUrls());
if (attachmentUserMessage != null) {
chatMessages.add(attachmentUserMessage);
} }
} }
// 2.1 查询 tool 工具
List<ToolCallback> toolCallbacks = getToolCallbackListByRoleId(conversation.getRoleId());
Map<String,Object> toolContext = CollUtil.isNotEmpty(toolCallbacks) ? AiUtils.buildCommonToolContext()
: Map.of();
// 2.2 构建 ChatOptions 对象 // 2.2 构建 ChatOptions 对象
AiPlatformEnum platform = AiPlatformEnum.validatePlatform(model.getPlatform()); AiPlatformEnum platform = AiPlatformEnum.validatePlatform(model.getPlatform());
ChatOptions chatOptions = AiUtils.buildChatOptions(platform, model.getModel(), ChatOptions chatOptions = AiUtils.buildChatOptions(platform, model.getModel(),
conversation.getTemperature(), conversation.getMaxTokens(), toolNames, toolContext); conversation.getTemperature(), conversation.getMaxTokens(),
toolCallbacks, toolContext);
return new Prompt(chatMessages, chatOptions); return new Prompt(chatMessages, chatOptions);
} }
private List<ToolCallback> getToolCallbackListByRoleId(Long roleId) {
if (roleId == null) {
return null;
}
AiChatRoleDO chatRole = chatRoleService.getChatRole(roleId);
if (chatRole == null) {
return null;
}
List<ToolCallback> toolCallbacks = new ArrayList<>();
// 1. 通过 toolIds
if (CollUtil.isNotEmpty(chatRole.getToolIds())) {
Set<String> toolNames = convertSet(toolService.getToolList(chatRole.getToolIds()), AiToolDO::getName);
toolNames.forEach(toolName -> {
ToolCallback toolCallback = toolCallbackResolver.resolve(toolName);
if (toolCallback != null) {
toolCallbacks.add(toolCallback);
}
});
}
// 2. 通过 mcpClients
if (CollUtil.isNotEmpty(mcpClients) && CollUtil.isNotEmpty(chatRole.getMcpClientNames())) {
chatRole.getMcpClientNames().forEach(mcpClientName -> {
// 2.1 标准化名字参考 McpClientAutoConfiguration connectedClientName 方法
String finalMcpClientName = mcpClientCommonProperties.getName() + " - " + mcpClientName;
// 2.2 匹配对应的 McpSyncClient
mcpClients.forEach(mcpClient -> {
if (ObjUtil.notEqual(mcpClient.getClientInfo().name(), finalMcpClientName)) {
return;
}
ToolCallback[] mcpToolCallBacks = new SyncMcpToolCallbackProvider(mcpClient).getToolCallbacks();
CollUtil.addAll(toolCallbacks, mcpToolCallBacks);
});
});
}
return toolCallbacks;
}
/** /**
* 从历史消息中获得倒序的 n 组消息作为消息上下文 * 从历史消息中获得倒序的 n 组消息作为消息上下文
* <p> * <p>
@ -302,14 +458,56 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
return contextMessages; return contextMessages;
} }
private UserMessage buildAttachmentUserMessage(List<String> attachmentUrls) {
if (CollUtil.isEmpty(attachmentUrls)) {
return null;
}
// 读取文件内容
Map<String, String> attachmentContents = Maps.newLinkedHashMapWithExpectedSize(attachmentUrls.size());
for (String attachmentUrl : attachmentUrls) {
try {
String name = FileNameUtil.getName(attachmentUrl);
String mineType = FileTypeUtils.getMineType(name);
String content;
if (FileTypeUtils.isImage(mineType)) {
// 特殊图片则转为 Base64
byte[] bytes = HttpUtil.downloadBytes(attachmentUrl);
content = Base64.encode(bytes);
} else {
content = knowledgeDocumentService.readUrl(attachmentUrl);
}
if (StrUtil.isNotEmpty(content)) {
attachmentContents.put(name, content);
}
} catch (Exception e) {
log.error("[buildAttachmentUserMessage][读取附件({}) 发生异常]", attachmentUrl, e);
}
}
if (CollUtil.isEmpty(attachmentContents)) {
return null;
}
// 拼接 UserMessage 消息
String attachment = attachmentContents.entrySet().stream()
.map(entry -> "<Attachment name=\"" + entry.getKey() + "\">" + entry.getValue() + "</Attachment>")
.collect(Collectors.joining("\n\n"));
return new UserMessage(String.format(Attachment_USER_MESSAGE_TEMPLATE, attachment));
}
private AiChatMessageDO createChatMessage(Long conversationId, Long replyId, private AiChatMessageDO createChatMessage(Long conversationId, Long replyId,
AiModelDO model, Long userId, Long roleId, AiModelDO model, Long userId, Long roleId,
MessageType messageType, String content, Boolean useContext, MessageType messageType, String content, Boolean useContext,
List<AiKnowledgeSegmentSearchRespBO> knowledgeSegments) { List<AiKnowledgeSegmentSearchRespBO> knowledgeSegments,
List<String> attachmentUrls,
AiWebSearchResponse webSearchResponse) {
AiChatMessageDO message = new AiChatMessageDO().setConversationId(conversationId).setReplyId(replyId) AiChatMessageDO message = new AiChatMessageDO().setConversationId(conversationId).setReplyId(replyId)
.setModel(model.getModel()).setModelId(model.getId()).setUserId(userId).setRoleId(roleId) .setModel(model.getModel()).setModelId(model.getId()).setUserId(userId).setRoleId(roleId)
.setType(messageType.getValue()).setContent(content).setUseContext(useContext) .setType(messageType.getValue()).setContent(content).setUseContext(useContext)
.setSegmentIds(convertList(knowledgeSegments, AiKnowledgeSegmentSearchRespBO::getId)); .setSegmentIds(convertList(knowledgeSegments, AiKnowledgeSegmentSearchRespBO::getId))
.setAttachmentUrls(attachmentUrls);
if (webSearchResponse != null) {
message.setWebSearchPages(webSearchResponse.getLists());
}
message.setCreateTime(LocalDateTime.now()); message.setCreateTime(LocalDateTime.now());
chatMessageMapper.insert(message); chatMessageMapper.insert(message);
return message; return message;

View File

@ -9,9 +9,6 @@ import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil; import cn.hutool.extra.spring.SpringUtil;
import cn.hutool.http.HttpUtil; import cn.hutool.http.HttpUtil;
import cn.iocoder.yudao.module.ai.enums.model.AiPlatformEnum;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.midjourney.api.MidjourneyApi;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.siliconflow.SiliconFlowImageOptions;
import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageDrawReqVO; import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageDrawReqVO;
@ -24,6 +21,9 @@ import cn.iocoder.yudao.module.ai.dal.dataobject.image.AiImageDO;
import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiModelDO; import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiModelDO;
import cn.iocoder.yudao.module.ai.dal.mysql.image.AiImageMapper; import cn.iocoder.yudao.module.ai.dal.mysql.image.AiImageMapper;
import cn.iocoder.yudao.module.ai.enums.image.AiImageStatusEnum; import cn.iocoder.yudao.module.ai.enums.image.AiImageStatusEnum;
import cn.iocoder.yudao.module.ai.enums.model.AiPlatformEnum;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.midjourney.api.MidjourneyApi;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.siliconflow.SiliconFlowImageOptions;
import cn.iocoder.yudao.module.ai.service.model.AiModelService; import cn.iocoder.yudao.module.ai.service.model.AiModelService;
import cn.iocoder.yudao.module.infra.api.file.FileApi; import cn.iocoder.yudao.module.infra.api.file.FileApi;
import com.alibaba.cloud.ai.dashscope.image.DashScopeImageOptions; import com.alibaba.cloud.ai.dashscope.image.DashScopeImageOptions;
@ -89,7 +89,7 @@ public class AiImageServiceImpl implements AiImageService {
if (CollUtil.isEmpty(ids)) { if (CollUtil.isEmpty(ids)) {
return Collections.emptyList(); return Collections.emptyList();
} }
return imageMapper.selectBatchIds(ids); return imageMapper.selectByIds(ids);
} }
@Override @Override

View File

@ -18,7 +18,11 @@ import cn.iocoder.yudao.module.ai.dal.mysql.knowledge.AiKnowledgeSegmentMapper;
import cn.iocoder.yudao.module.ai.service.knowledge.bo.AiKnowledgeSegmentSearchReqBO; import cn.iocoder.yudao.module.ai.service.knowledge.bo.AiKnowledgeSegmentSearchReqBO;
import cn.iocoder.yudao.module.ai.service.knowledge.bo.AiKnowledgeSegmentSearchRespBO; import cn.iocoder.yudao.module.ai.service.knowledge.bo.AiKnowledgeSegmentSearchRespBO;
import cn.iocoder.yudao.module.ai.service.model.AiModelService; import cn.iocoder.yudao.module.ai.service.model.AiModelService;
import javax.annotation.Resource; import com.alibaba.cloud.ai.dashscope.rerank.DashScopeRerankOptions;
import com.alibaba.cloud.ai.model.RerankModel;
import com.alibaba.cloud.ai.model.RerankRequest;
import com.alibaba.cloud.ai.model.RerankResponse;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.document.Document; import org.springframework.ai.document.Document;
import org.springframework.ai.tokenizer.TokenCountEstimator; import org.springframework.ai.tokenizer.TokenCountEstimator;
@ -27,6 +31,7 @@ import org.springframework.ai.transformer.splitter.TokenTextSplitter;
import org.springframework.ai.vectorstore.SearchRequest; import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.VectorStore; import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.ai.vectorstore.filter.FilterExpressionBuilder; import org.springframework.ai.vectorstore.filter.FilterExpressionBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@ -36,6 +41,7 @@ import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionU
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.KNOWLEDGE_SEGMENT_CONTENT_TOO_LONG; import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.KNOWLEDGE_SEGMENT_CONTENT_TOO_LONG;
import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.KNOWLEDGE_SEGMENT_NOT_EXISTS; import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.KNOWLEDGE_SEGMENT_NOT_EXISTS;
import static org.springframework.ai.vectorstore.SearchRequest.SIMILARITY_THRESHOLD_ACCEPT_ALL;
/** /**
* AI 知识库分片 Service 实现类 * AI 知识库分片 Service 实现类
@ -55,6 +61,11 @@ public class AiKnowledgeSegmentServiceImpl implements AiKnowledgeSegmentService
VECTOR_STORE_METADATA_DOCUMENT_ID, String.class, VECTOR_STORE_METADATA_DOCUMENT_ID, String.class,
VECTOR_STORE_METADATA_SEGMENT_ID, String.class); VECTOR_STORE_METADATA_SEGMENT_ID, String.class);
/**
* Rerank 在向量检索时检索数量 * 该系数目的是为了提升 Rerank 的效果
*/
private static final Integer RERANK_RETRIEVAL_FACTOR = 4;
@Resource @Resource
private AiKnowledgeSegmentMapper segmentMapper; private AiKnowledgeSegmentMapper segmentMapper;
@ -69,6 +80,9 @@ public class AiKnowledgeSegmentServiceImpl implements AiKnowledgeSegmentService
@Resource @Resource
private TokenCountEstimator tokenCountEstimator; private TokenCountEstimator tokenCountEstimator;
@Autowired(required = false) // 由于 spring.ai.model.rerank 配置项可以关闭 RerankModel 的功能所以这里只能不强制注入
private RerankModel rerankModel;
@Override @Override
public PageResult<AiKnowledgeSegmentDO> getKnowledgeSegmentPage(AiKnowledgeSegmentPageReqVO pageReqVO) { public PageResult<AiKnowledgeSegmentDO> getKnowledgeSegmentPage(AiKnowledgeSegmentPageReqVO pageReqVO) {
return segmentMapper.selectPage(pageReqVO); return segmentMapper.selectPage(pageReqVO);
@ -211,28 +225,16 @@ public class AiKnowledgeSegmentServiceImpl implements AiKnowledgeSegmentService
// 1. 校验 // 1. 校验
AiKnowledgeDO knowledge = knowledgeService.validateKnowledgeExists(reqBO.getKnowledgeId()); AiKnowledgeDO knowledge = knowledgeService.validateKnowledgeExists(reqBO.getKnowledgeId());
// 2.1 向量检索 // 2. 检索
VectorStore vectorStore = getVectorStoreById(knowledge); List<Document> documents = searchDocument(knowledge, reqBO);
List<Document> documents = vectorStore.similaritySearch(SearchRequest.builder()
.query(reqBO.getContent()) // 3.1 段落召回
.topK(ObjUtil.defaultIfNull(reqBO.getTopK(), knowledge.getTopK()))
.similarityThreshold(
ObjUtil.defaultIfNull(reqBO.getSimilarityThreshold(), knowledge.getSimilarityThreshold()))
.filterExpression(new FilterExpressionBuilder()
.eq(VECTOR_STORE_METADATA_KNOWLEDGE_ID, reqBO.getKnowledgeId().toString())
.build())
.build());
if (CollUtil.isEmpty(documents)) {
return ListUtil.empty();
}
// 2.2 段落召回
List<AiKnowledgeSegmentDO> segments = segmentMapper List<AiKnowledgeSegmentDO> segments = segmentMapper
.selectListByVectorIds(convertList(documents, Document::getId)); .selectListByVectorIds(convertList(documents, Document::getId));
if (CollUtil.isEmpty(segments)) { if (CollUtil.isEmpty(segments)) {
return ListUtil.empty(); return ListUtil.empty();
} }
// 3.2 增加召回次数
// 3. 增加召回次数
segmentMapper.updateRetrievalCountIncrByIds(convertList(segments, AiKnowledgeSegmentDO::getId)); segmentMapper.updateRetrievalCountIncrByIds(convertList(segments, AiKnowledgeSegmentDO::getId));
// 4. 构建结果 // 4. 构建结果
@ -249,6 +251,42 @@ public class AiKnowledgeSegmentServiceImpl implements AiKnowledgeSegmentService
return result; return result;
} }
/**
* 基于 Embedding + Rerank Model检索知识库中的文档
*
* @param knowledge 知识库
* @param reqBO 检索请求
* @return 文档列表
*/
private List<Document> searchDocument(AiKnowledgeDO knowledge, AiKnowledgeSegmentSearchReqBO reqBO) {
VectorStore vectorStore = getVectorStoreById(knowledge);
Integer topK = ObjUtil.defaultIfNull(reqBO.getTopK(), knowledge.getTopK());
Double similarityThreshold = ObjUtil.defaultIfNull(reqBO.getSimilarityThreshold(), knowledge.getSimilarityThreshold());
// 1. 向量检索
int searchTopK = rerankModel != null ? topK * RERANK_RETRIEVAL_FACTOR : topK;
double searchSimilarityThreshold = rerankModel != null ? SIMILARITY_THRESHOLD_ACCEPT_ALL : similarityThreshold;
SearchRequest.Builder searchRequestBuilder = SearchRequest.builder()
.query(reqBO.getContent())
.topK(searchTopK).similarityThreshold(searchSimilarityThreshold)
.filterExpression(new FilterExpressionBuilder()
.eq(VECTOR_STORE_METADATA_KNOWLEDGE_ID, reqBO.getKnowledgeId().toString()).build());
List<Document> documents = vectorStore.similaritySearch(searchRequestBuilder.build());
if (CollUtil.isEmpty(documents)) {
return documents;
}
// 2. Rerank 重排序
if (rerankModel != null) {
RerankResponse rerankResponse = rerankModel.call(new RerankRequest(reqBO.getContent(), documents,
DashScopeRerankOptions.builder().withTopN(topK).build()));
documents = convertList(rerankResponse.getResults(),
documentWithScore -> documentWithScore.getScore() >= similarityThreshold
? documentWithScore.getOutput() : null);
}
return documents;
}
@Override @Override
public List<AiKnowledgeSegmentDO> splitContent(String url, Integer segmentMaxTokens) { public List<AiKnowledgeSegmentDO> splitContent(String url, Integer segmentMaxTokens) {
// 1. 读取 URL 内容 // 1. 读取 URL 内容

View File

@ -1,7 +1,7 @@
package cn.iocoder.yudao.module.ai.service.model; package cn.iocoder.yudao.module.ai.service.model;
import cn.iocoder.yudao.module.ai.enums.model.AiPlatformEnum; import cn.iocoder.yudao.module.ai.enums.model.AiPlatformEnum;
import cn.iocoder.yudao.module.ai.framework.ai.core.AiModelFactory; import cn.iocoder.yudao.module.ai.framework.ai.core.model.AiModelFactory;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.midjourney.api.MidjourneyApi; import cn.iocoder.yudao.module.ai.framework.ai.core.model.midjourney.api.MidjourneyApi;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.suno.api.SunoApi; import cn.iocoder.yudao.module.ai.framework.ai.core.model.suno.api.SunoApi;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;

View File

@ -1,14 +1,14 @@
package cn.iocoder.yudao.module.ai.service.model; package cn.iocoder.yudao.module.ai.service.model;
import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.ai.controller.admin.model.vo.tool.AiToolPageReqVO; import cn.iocoder.yudao.module.ai.controller.admin.model.vo.tool.AiToolPageReqVO;
import cn.iocoder.yudao.module.ai.controller.admin.model.vo.tool.AiToolSaveReqVO; import cn.iocoder.yudao.module.ai.controller.admin.model.vo.tool.AiToolSaveReqVO;
import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiToolDO; import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiToolDO;
import cn.iocoder.yudao.module.ai.dal.mysql.model.AiToolMapper; import cn.iocoder.yudao.module.ai.dal.mysql.model.AiToolMapper;
import javax.annotation.Resource; import jakarta.annotation.Resource;
import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.ai.tool.ToolCallback;
import org.springframework.ai.tool.resolution.ToolCallbackResolver;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
@ -31,6 +31,9 @@ public class AiToolServiceImpl implements AiToolService {
@Resource @Resource
private AiToolMapper toolMapper; private AiToolMapper toolMapper;
@Resource
private ToolCallbackResolver toolCallbackResolver;
@Override @Override
public Long createTool(AiToolSaveReqVO createReqVO) { public Long createTool(AiToolSaveReqVO createReqVO) {
// 校验名称是否存在 // 校验名称是否存在
@ -70,9 +73,8 @@ public class AiToolServiceImpl implements AiToolService {
} }
private void validateToolNameExists(String name) { private void validateToolNameExists(String name) {
try { ToolCallback toolCallback = toolCallbackResolver.resolve(name);
SpringUtil.getBean(name); if (toolCallback == null) {
} catch (NoSuchBeanDefinitionException e) {
throw exception(TOOL_NAME_NOT_EXISTS, name); throw exception(TOOL_NAME_NOT_EXISTS, name);
} }
} }

View File

@ -1,4 +1,4 @@
package cn.iocoder.yudao.module.ai.service.model.tool; package cn.iocoder.yudao.module.ai.tool.function;
import cn.hutool.core.date.LocalDateTimeUtil; import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.io.FileUtil; import cn.hutool.core.io.FileUtil;

View File

@ -1,4 +1,4 @@
package cn.iocoder.yudao.module.ai.service.model.tool; package cn.iocoder.yudao.module.ai.tool.function;
import cn.iocoder.yudao.module.ai.util.AiUtils; import cn.iocoder.yudao.module.ai.util.AiUtils;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.framework.common.util.object.BeanUtils;

View File

@ -1,4 +1,4 @@
package cn.iocoder.yudao.module.ai.service.model.tool; package cn.iocoder.yudao.module.ai.tool.function;
import cn.hutool.core.date.LocalDateTimeUtil; import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.util.RandomUtil; import cn.hutool.core.util.RandomUtil;

View File

@ -0,0 +1,4 @@
/**
* 参考 <a href="https://docs.spring.io/spring-ai/reference/api/tools.html#_methods_as_tools">Tool Calling Methods as Tools</a>
*/
package cn.iocoder.yudao.module.ai.tool.function;

View File

@ -0,0 +1,19 @@
package cn.iocoder.yudao.module.ai.tool.method;
/**
* 来自 Spring AI 官方文档
*
* Represents a person with basic information.
* This is an immutable record.
*/
public record Person(
int id,
String firstName,
String lastName,
String email,
String sex,
String ipAddress,
String jobTitle,
int age
) {
}

View File

@ -0,0 +1,80 @@
package cn.iocoder.yudao.module.ai.tool.method;
import java.util.List;
import java.util.Optional;
/**
* 来自 Spring AI 官方文档
*
* Service interface for managing Person data.
* Defines the contract for CRUD operations and search/filter functionalities.
*/
public interface PersonService {
/**
* Creates a new Person record.
* Assigns a unique ID to the person and stores it.
*
* @param personData The data for the new person (ID field is ignored). Must not be null.
* @return The created Person record, including the generated ID.
*/
Person createPerson(Person personData);
/**
* Retrieves a Person by their unique ID.
*
* @param id The ID of the person to retrieve.
* @return An Optional containing the found Person, or an empty Optional if not found.
*/
Optional<Person> getPersonById(int id);
/**
* Retrieves all Person records currently stored.
*
* @return An unmodifiable List containing all Persons. Returns an empty list if none exist.
*/
List<Person> getAllPersons();
/**
* Updates an existing Person record identified by ID.
* Replaces the existing data with the provided data, keeping the original ID.
*
* @param id The ID of the person to update.
* @param updatedPersonData The new data for the person (ID field is ignored). Must not be null.
* @return true if the person was found and updated, false otherwise.
*/
boolean updatePerson(int id, Person updatedPersonData);
/**
* Deletes a Person record identified by ID.
*
* @param id The ID of the person to delete.
* @return true if the person was found and deleted, false otherwise.
*/
boolean deletePerson(int id);
/**
* Searches for Persons whose job title contains the given query string (case-insensitive).
*
* @param jobTitleQuery The string to search for within job titles. Can be null or blank.
* @return An unmodifiable List of matching Persons. Returns an empty list if no matches or query is invalid.
*/
List<Person> searchByJobTitle(String jobTitleQuery);
/**
* Filters Persons by their exact sex (case-insensitive).
*
* @param sex The sex to filter by (e.g., "Male", "Female"). Can be null or blank.
* @return An unmodifiable List of matching Persons. Returns an empty list if no matches or filter is invalid.
*/
List<Person> filterBySex(String sex);
/**
* Filters Persons by their exact age.
*
* @param age The age to filter by.
* @return An unmodifiable List of matching Persons. Returns an empty list if no matches.
*/
List<Person> filterByAge(int age);
}

View File

@ -0,0 +1,336 @@
package cn.iocoder.yudao.module.ai.tool.method;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.stereotype.Service;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* 来自 Spring AI 官方文档
*
* Implementation of the PersonService interface using an in-memory data store.
* Manages a collection of Person objects loaded from embedded CSV data.
* This class is thread-safe due to the use of ConcurrentHashMap and AtomicInteger.
*/
@Service
@Slf4j
public class PersonServiceImpl implements PersonService {
private final Map<Integer, Person> personStore = new ConcurrentHashMap<>();
private AtomicInteger idGenerator;
/**
* Embedded CSV data for initial population
*/
private static final String CSV_DATA = """
Id,FirstName,LastName,Email,Sex,IpAddress,JobTitle,Age
1,Fons,Tollfree,ftollfree0@senate.gov,Male,55.1 Tollfree Lane,Research Associate,31
2,Emlynne,Tabourier,etabourier1@networksolutions.com,Female,18 Tabourier Way,Associate Professor,38
3,Shae,Johncey,sjohncey2@yellowpages.com,Male,1 Johncey Circle,Structural Analysis Engineer,30
4,Sebastien,Bradly,sbradly3@mapquest.com,Male,2 Bradly Hill,Chief Executive Officer,40
5,Harriott,Kitteringham,hkitteringham4@typepad.com,Female,3 Kitteringham Drive,VP Sales,47
6,Anallise,Parradine,aparradine5@miibeian.gov.cn,Female,4 Parradine Street,Analog Circuit Design manager,44
7,Gorden,Kirkbright,gkirkbright6@reuters.com,Male,5 Kirkbright Plaza,Senior Editor,40
8,Veradis,Ledwitch,vledwitch7@google.com.au,Female,6 Ledwitch Avenue,Computer Systems Analyst IV,44
9,Agnesse,Penhalurick,apenhalurick8@google.it,Female,7 Penhalurick Terrace,Automation Specialist IV,41
10,Bibby,Hutable,bhutable9@craigslist.org,Female,8 Hutable Place,Account Representative I,43
11,Karoly,Lightoller,klightollera@rakuten.co.jp,Female,9 Lightoller Parkway,Senior Developer,46
12,Cristine,Durrad,cdurradb@aol.com,Female,10 Durrad Center,Senior Developer,48
13,Aggy,Napier,anapierc@hostgator.com,Female,11 Napier Court,VP Product Management,44
14,Prisca,Caddens,pcaddensd@vinaora.com,Female,12 Caddens Alley,Business Systems Development Analyst,41
15,Khalil,McKernan,kmckernane@google.fr,Male,13 McKernan Pass,Engineer IV,44
16,Lorry,MacTrusty,lmactrustyf@eventbrite.com,Male,14 MacTrusty Junction,Design Engineer,42
17,Casandra,Worsell,cworsellg@goo.gl,Female,15 Worsell Point,Systems Administrator IV,45
18,Ulrikaumeko,Haveline,uhavelineh@usgs.gov,Female,16 Haveline Trail,Financial Advisor,42
19,Shurlocke,Albany,salbanyi@artisteer.com,Male,17 Albany Plaza,Software Test Engineer III,46
20,Myrilla,Brimilcombe,mbrimilcombej@accuweather.com,Female,18 Brimilcombe Road,Programmer Analyst I,48
21,Carlina,Scimonelli,cscimonellik@va.gov,Female,19 Scimonelli Pass,Help Desk Technician,45
22,Tina,Goullee,tgoulleel@miibeian.gov.cn,Female,20 Goullee Crossing,Accountant IV,43
23,Adriaens,Storek,astorekm@devhub.com,Female,21 Storek Avenue,Recruiting Manager,40
24,Tedra,Giraudot,tgiraudotn@wiley.com,Female,22 Giraudot Terrace,Speech Pathologist,47
25,Josiah,Soares,jsoareso@google.nl,Male,23 Soares Street,Tax Accountant,45
26,Kayle,Gaukrodge,kgaukrodgep@wikispaces.com,Female,24 Gaukrodge Parkway,Accountant II,43
27,Ardys,Chuter,achuterq@ustream.tv,Female,25 Chuter Drive,Engineer IV,41
28,Francyne,Baudinet,fbaudinetr@newyorker.com,Female,26 Baudinet Center,VP Accounting,48
29,Gerick,Bullan,gbullans@seesaa.net,Male,27 Bullan Way,Senior Financial Analyst,43
30,Northrup,Grivori,ngrivorit@unc.edu,Male,28 Grivori Plaza,Systems Administrator I,45
31,Town,Duguid,tduguidu@squarespace.com,Male,29 Duguid Pass,Safety Technician IV,46
32,Pierette,Kopisch,pkopischv@google.com.br,Female,30 Kopisch Lane,Director of Sales,41
33,Jacquenetta,Le Prevost,jleprevostw@netlog.com,Female,31 Le Prevost Trail,Senior Developer,47
34,Garvy,Rusted,grustedx@aboutads.info,Male,32 Rusted Junction,Senior Developer,42
35,Clarice,Aysh,cayshy@merriam-webster.com,Female,33 Aysh Avenue,VP Quality Control,40
36,Tracie,Fedorski,tfedorskiz@bloglines.com,Male,34 Fedorski Terrace,Design Engineer,44
37,Noelyn,Matushenko,nmatushenko10@globo.com,Female,35 Matushenko Place,VP Sales,48
38,Rudiger,Klaesson,rklaesson11@usnews.com,Male,36 Klaesson Road,Database Administrator IV,43
39,Mirella,Syddie,msyddie12@geocities.jp,Female,37 Syddie Circle,Geological Engineer,46
40,Donalt,O'Lunny,dolunny13@elpais.com,Male,38 O'Lunny Center,Analog Circuit Design manager,41
41,Guntar,Deniskevich,gdeniskevich14@google.com.hk,Male,39 Deniskevich Way,Structural Engineer,47
42,Hort,Shufflebotham,hshufflebotham15@about.me,Male,40 Shufflebotham Court,Structural Analysis Engineer,45
43,Dominique,Thickett,dthickett16@slashdot.org,Male,41 Thickett Crossing,Safety Technician I,42
44,Zebulen,Piscopello,zpiscopello17@umich.edu,Male,42 Piscopello Parkway,Web Developer II,40
45,Mellicent,Mac Giany,mmacgiany18@state.tx.us,Female,43 Mac Giany Pass,Assistant Manager,44
46,Merle,Bounds,mbounds19@amazon.co.jp,Female,44 Bounds Alley,Systems Administrator III,41
47,Madelle,Farbrace,mfarbrace1a@xinhuanet.com,Female,45 Farbrace Terrace,Quality Engineer,48
48,Galvin,O'Sheeryne,gosheeryne1b@addtoany.com,Male,46 O'Sheeryne Way,Environmental Specialist,43
49,Guillemette,Bootherstone,gbootherstone1c@nationalgeographic.com,Female,47 Bootherstone Plaza,Professor,46
50,Letti,Aylmore,laylmore1d@vinaora.com,Female,48 Aylmore Circle,Automation Specialist I,40
51,Nonie,Rivalland,nrivalland1e@weather.com,Female,49 Rivalland Avenue,Software Test Engineer IV,45
52,Jacquelynn,Halfacre,jhalfacre1f@surveymonkey.com,Female,50 Halfacre Pass,Geologist II,42
53,Anderea,MacKibbon,amackibbon1g@weibo.com,Female,51 MacKibbon Parkway,Automation Specialist II,47
54,Wash,Klimko,wklimko1h@slashdot.org,Male,52 Klimko Alley,Database Administrator I,40
55,Flori,Kynett,fkynett1i@auda.org.au,Female,53 Kynett Trail,Quality Control Specialist,46
56,Libbey,Penswick,lpenswick1j@google.co.uk,Female,54 Penswick Point,VP Accounting,43
57,Silvanus,Skellorne,sskellorne1k@booking.com,Male,55 Skellorne Drive,Account Executive,48
58,Carmine,Mateos,cmateos1l@plala.or.jp,Male,56 Mateos Terrace,Systems Administrator I,41
59,Sheffie,Blazewicz,sblazewicz1m@google.com.au,Male,57 Blazewicz Center,VP Sales,44
60,Leanor,Worsnop,lworsnop1n@uol.com.br,Female,58 Worsnop Plaza,Systems Administrator III,45
61,Caspar,Pamment,cpamment1o@google.co.jp,Male,59 Pamment Court,Senior Financial Analyst,42
62,Justinian,Pentycost,jpentycost1p@sciencedaily.com,Male,60 Pentycost Way,Senior Quality Engineer,47
63,Gerianne,Jarnell,gjarnell1q@bing.com,Female,61 Jarnell Avenue,Help Desk Operator,40
64,Boycie,Zanetto,bzanetto1r@about.com,Male,62 Zanetto Place,Quality Engineer,46
65,Camilla,Mac Giany,cmacgiany1s@state.gov,Female,63 Mac Giany Parkway,Senior Cost Accountant,43
66,Hadlee,Piscopiello,hpiscopiello1t@artisteer.com,Male,64 Piscopiello Street,Account Representative III,48
67,Bobbie,Penvarden,bpenvarden1u@google.cn,Male,65 Penvarden Lane,Help Desk Operator,41
68,Ali,Gowlett,agowlett1v@parallels.com,Male,66 Gowlett Pass,VP Marketing,44
69,Olivette,Acome,oacome1w@qq.com,Female,67 Acome Hill,VP Product Management,45
70,Jehanna,Brotherheed,jbrotherheed1x@google.nl,Female,68 Brotherheed Junction,Database Administrator III,42
71,Morgan,Berthomieu,mberthomieu1y@artisteer.com,Male,69 Berthomieu Alley,Systems Administrator II,47
72,Linzy,Shilladay,lshilladay1z@icq.com,Female,70 Shilladay Trail,Research Assistant IV,40
73,Faydra,Brimner,fbrimner20@mozilla.org,Female,71 Brimner Road,Senior Editor,46
74,Gwenore,Oxlee,goxlee21@devhub.com,Female,72 Oxlee Terrace,Systems Administrator II,43
75,Evangelin,Beinke,ebeinke22@mozilla.com,Female,73 Beinke Circle,Accountant I,48
76,Missy,Cockling,mcockling23@si.edu,Female,74 Cockling Way,Software Engineer I,41
77,Suzanne,Klimschak,sklimschak24@etsy.com,Female,75 Klimschak Plaza,Tax Accountant,44
78,Candide,Goricke,cgoricke25@weebly.com,Female,76 Goricke Pass,Sales Associate,45
79,Gerome,Pinsent,gpinsent26@google.com.au,Male,77 Pinsent Junction,Software Consultant,42
80,Lezley,Mac Giany,lmacgiany27@scribd.com,Male,78 Mac Giany Alley,Operator,47
81,Tobiah,Durn,tdurn28@state.tx.us,Male,79 Durn Court,VP Sales,40
82,Sherlocke,Cockshoot,scockshoot29@yelp.com,Male,80 Cockshoot Street,Senior Financial Analyst,46
83,Myrle,Speenden,mspeenden2a@utexas.edu,Female,81 Speenden Center,Senior Developer,43
84,Isidore,Gorries,igorries2b@flavors.me,Male,82 Gorries Parkway,Sales Representative,48
85,Isac,Kitchingman,ikitchingman2c@businessinsider.com,Male,83 Kitchingman Drive,VP Accounting,41
86,Benedetta,Purrier,bpurrier2d@admin.ch,Female,84 Purrier Trail,VP Accounting,44
87,Tera,Fitchell,tfitchell2e@fotki.com,Female,85 Fitchell Place,Software Engineer IV,45
88,Abbe,Pamment,apamment2f@about.com,Male,86 Pamment Avenue,VP Sales,42
89,Jandy,Gommowe,jgommowe2g@angelfire.com,Female,87 Gommowe Road,Financial Analyst,47
90,Karena,Fussey,kfussey2h@google.com.au,Female,88 Fussey Point,Assistant Professor,40
91,Gaspar,Pammenter,gpammenter2i@google.com.br,Male,89 Pammenter Hill,Help Desk Operator,46
92,Stanwood,Mac Giany,smacgiany2j@prlog.org,Male,90 Mac Giany Terrace,Research Associate,43
93,Byrom,Beedell,bbeedell2k@google.co.jp,Male,91 Beedell Way,VP Sales,48
94,Annabella,Rowbottom,arowbottom2l@google.com.au,Female,92 Rowbottom Plaza,Help Desk Operator,41
95,Rodolphe,Debell,rdebell2m@imageshack.us,Male,93 Debell Pass,Design Engineer,44
96,Tyne,Gommey,tgommey2n@joomla.org,Female,94 Gommey Junction,VP Marketing,45
97,Christoper,Pincked,cpincked2o@icq.com,Male,95 Pincked Alley,Human Resources Manager,42
98,Kore,Le Prevost,kleprevost2p@tripadvisor.com,Female,96 Le Prevost Street,VP Quality Control,47
99,Ceciley,Petrolli,cpetrolli2q@oaic.gov.au,Female,97 Petrolli Court,Senior Developer,40
100,Elspeth,Mac Giany,emacgiany2r@icio.us,Female,98 Mac Giany Parkway,Internal Auditor,46
""";
/**
* Initializes the service after dependency injection by loading data from the CSV string.
* Uses @PostConstruct to ensure this runs after the bean is created.
*/
@PostConstruct
private void initializeData() {
log.info("Initializing PersonService data store...");
int maxId = loadDataFromCsv();
idGenerator = new AtomicInteger(maxId);
log.info("PersonService initialized with {} records. Next ID: {}", personStore.size(), idGenerator.get() + 1);
}
/**
* Parses the embedded CSV data and populates the in-memory store.
* Calculates the maximum ID found in the data to initialize the ID generator.
*
* @return The maximum ID found in the loaded CSV data.
*/
private int loadDataFromCsv() {
final AtomicInteger currentMaxId = new AtomicInteger(0);
// Clear existing data before loading (important for tests or re-initialization scenarios)
personStore.clear();
try (Stream<String> lines = CSV_DATA.lines().skip(1)) { // Skip header row
lines.forEach(line -> {
try {
// Split carefully, handling potential commas within quoted fields if necessary (simple split here)
String[] fields = line.split(",", 8); // Limit split to handle potential commas in job title
if (fields.length == 8) {
int id = Integer.parseInt(fields[0].trim());
String firstName = fields[1].trim();
String lastName = fields[2].trim();
String email = fields[3].trim();
String sex = fields[4].trim();
String ipAddress = fields[5].trim();
String jobTitle = fields[6].trim();
int age = Integer.parseInt(fields[7].trim());
Person person = new Person(id, firstName, lastName, email, sex, ipAddress, jobTitle, age);
personStore.put(id, person);
currentMaxId.updateAndGet(max -> Math.max(max, id));
} else {
log.warn("Skipping malformed CSV line (expected 8 fields, found {}): {}", fields.length, line);
}
} catch (NumberFormatException e) {
log.warn("Skipping line due to parsing error (ID or Age): {} - Error: {}", line, e.getMessage());
} catch (Exception e) {
log.error("Skipping line due to unexpected error: {} - Error: {}", line, e.getMessage(), e);
}
});
} catch (Exception e) {
log.error("Fatal error reading embedded CSV data: {}", e.getMessage(), e);
// In a real application, might throw a specific initialization exception
}
return currentMaxId.get();
}
@Override
@Tool(
name = "ps_create_person",
description = "Create a new person record in the in-memory store."
)
public Person createPerson(Person personData) {
if (personData == null) {
throw new IllegalArgumentException("Person data cannot be null");
}
int newId = idGenerator.incrementAndGet();
// Create a new Person record using data from the input, but with the generated ID
Person newPerson = new Person(
newId,
personData.firstName(),
personData.lastName(),
personData.email(),
personData.sex(),
personData.ipAddress(),
personData.jobTitle(),
personData.age()
);
personStore.put(newId, newPerson);
log.debug("Created person: {}", newPerson);
return newPerson;
}
@Override
@Tool(
name = "ps_get_person_by_id",
description = "Retrieve a person record by ID from the in-memory store."
)
public Optional<Person> getPersonById(int id) {
Person person = personStore.get(id);
log.debug("Retrieved person by ID {}: {}", id, person);
return Optional.ofNullable(person);
}
@Override
@Tool(
name = "ps_get_all_persons",
description = "Retrieve all person records from the in-memory store."
)
public List<Person> getAllPersons() {
// Return an unmodifiable view of the values
List<Person> allPersons = personStore.values().stream().toList();
log.debug("Retrieved all persons (count: {})", allPersons.size());
return allPersons;
}
@Override
@Tool(
name = "ps_update_person",
description = "Update an existing person record by ID in the in-memory store."
)
public boolean updatePerson(int id, Person updatedPersonData) {
if (updatedPersonData == null) {
throw new IllegalArgumentException("Updated person data cannot be null");
}
// Use computeIfPresent for atomic update if the key exists
Person result = personStore.computeIfPresent(id, (key, existingPerson) ->
// Create a new Person record with the original ID but updated data
new Person(
id, // Keep original ID
updatedPersonData.firstName(),
updatedPersonData.lastName(),
updatedPersonData.email(),
updatedPersonData.sex(),
updatedPersonData.ipAddress(),
updatedPersonData.jobTitle(),
updatedPersonData.age()
)
);
boolean updated = result != null;
log.debug("Update attempt for ID {}: {}", id, updated ? "Successful" : "Failed (Not Found)");
if(updated) log.trace("Updated person data for ID {}: {}", id, result);
return updated;
}
@Override
@Tool(
name = "ps_delete_person",
description = "Delete a person record by ID from the in-memory store."
)
public boolean deletePerson(int id) {
boolean removed = personStore.remove(id) != null;
log.debug("Delete attempt for ID {}: {}", id, removed ? "Successful" : "Failed (Not Found)");
return removed;
}
@Override
@Tool(
name = "ps_search_by_job_title",
description = "Search for persons by job title in the in-memory store."
)
public List<Person> searchByJobTitle(String jobTitleQuery) {
if (jobTitleQuery == null || jobTitleQuery.isBlank()) {
log.debug("Search by job title skipped due to blank query.");
return Collections.emptyList();
}
String lowerCaseQuery = jobTitleQuery.toLowerCase();
List<Person> results = personStore.values().stream()
.filter(person -> person.jobTitle() != null && person.jobTitle().toLowerCase().contains(lowerCaseQuery))
.collect(Collectors.toList());
log.debug("Search by job title '{}' found {} results.", jobTitleQuery, results.size());
return Collections.unmodifiableList(results);
}
@Override
@Tool(
name = "ps_filter_by_sex",
description = "Filters Persons by sex (case-insensitive)."
)
public List<Person> filterBySex(String sex) {
if (sex == null || sex.isBlank()) {
log.debug("Filter by sex skipped due to blank filter.");
return Collections.emptyList();
}
List<Person> results = personStore.values().stream()
.filter(person -> person.sex() != null && person.sex().equalsIgnoreCase(sex))
.collect(Collectors.toList());
log.debug("Filter by sex '{}' found {} results.", sex, results.size());
return Collections.unmodifiableList(results);
}
@Override
@Tool(
name = "ps_filter_by_age",
description = "Filters Persons by age."
)
public List<Person> filterByAge(int age) {
if (age < 0) {
log.debug("Filter by age skipped due to negative age: {}", age);
return Collections.emptyList(); // Or throw IllegalArgumentException based on requirements
}
List<Person> results = personStore.values().stream()
.filter(person -> person.age() == age)
.collect(Collectors.toList());
log.debug("Filter by age {} found {} results.", age, results.size());
return Collections.unmodifiableList(results);
}
}

View File

@ -0,0 +1,4 @@
/**
* 参考 <a href="https://docs.spring.io/spring-ai/reference/api/tools.html#_methods_as_tools">Tool Calling Methods as Tools</a>
*/
package cn.iocoder.yudao.module.ai.tool.method;

View File

@ -8,18 +8,20 @@ import cn.iocoder.yudao.module.ai.enums.model.AiPlatformEnum;
import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatOptions; import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatOptions;
import org.springaicommunity.moonshot.MoonshotChatOptions; import org.springaicommunity.moonshot.MoonshotChatOptions;
import org.springaicommunity.qianfan.QianFanChatOptions; import org.springaicommunity.qianfan.QianFanChatOptions;
import org.springframework.ai.anthropic.AnthropicChatOptions;
import org.springframework.ai.azure.openai.AzureOpenAiChatOptions; import org.springframework.ai.azure.openai.AzureOpenAiChatOptions;
import org.springframework.ai.chat.messages.*; import org.springframework.ai.chat.messages.*;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.ChatOptions; 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.minimax.MiniMaxChatOptions;
import org.springframework.ai.ollama.api.OllamaOptions; import org.springframework.ai.ollama.api.OllamaOptions;
import org.springframework.ai.openai.OpenAiChatOptions; import org.springframework.ai.openai.OpenAiChatOptions;
import org.springframework.ai.tool.ToolCallback;
import org.springframework.ai.zhipuai.ZhiPuAiChatOptions; import org.springframework.ai.zhipuai.ZhiPuAiChatOptions;
import java.util.Collections; import java.util.*;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
/** /**
* Spring AI 工具类 * Spring AI 工具类
@ -36,40 +38,47 @@ public class AiUtils {
} }
public static ChatOptions buildChatOptions(AiPlatformEnum platform, String model, Double temperature, Integer maxTokens, public static ChatOptions buildChatOptions(AiPlatformEnum platform, String model, Double temperature, Integer maxTokens,
Set<String> toolNames, Map<String, Object> toolContext) { List<ToolCallback> toolCallbacks, Map<String, Object> toolContext) {
toolNames = ObjUtil.defaultIfNull(toolNames, Collections.emptySet()); toolCallbacks = ObjUtil.defaultIfNull(toolCallbacks, Collections.emptyList());
toolContext = ObjUtil.defaultIfNull(toolContext, Collections.emptyMap()); toolContext = ObjUtil.defaultIfNull(toolContext, Collections.emptyMap());
// noinspection EnhancedSwitchMigration // noinspection EnhancedSwitchMigration
switch (platform) { switch (platform) {
case TONG_YI: case TONG_YI:
return DashScopeChatOptions.builder().withModel(model).withTemperature(temperature).withMaxToken(maxTokens) return DashScopeChatOptions.builder().withModel(model).withTemperature(temperature).withMaxToken(maxTokens)
.withToolNames(toolNames).withToolContext(toolContext).build(); .withEnableThinking(true) // TODO 芋艿默认都开启 thinking 模式后续可以让用户配置
.withToolCallbacks(toolCallbacks).withToolContext(toolContext).build();
case YI_YAN: case YI_YAN:
return QianFanChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens).build(); 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 XING_HUO: // 复用 DeepSeek 客户端
return DeepSeekChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens)
.toolCallbacks(toolCallbacks).toolContext(toolContext).build();
case ZHI_PU: case ZHI_PU:
return ZhiPuAiChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens) return ZhiPuAiChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens)
.toolNames(toolNames).toolContext(toolContext).build(); .toolCallbacks(toolCallbacks).toolContext(toolContext).build();
case MINI_MAX: case MINI_MAX:
return MiniMaxChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens) return MiniMaxChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens)
.toolNames(toolNames).toolContext(toolContext).build(); .toolCallbacks(toolCallbacks).toolContext(toolContext).build();
case MOONSHOT: case MOONSHOT:
return MoonshotChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens) return MoonshotChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens)
.toolNames(toolNames).toolContext(toolContext).build(); .toolCallbacks(toolCallbacks).toolContext(toolContext).build();
case OPENAI: case OPENAI:
case DEEP_SEEK: // 复用 OpenAI 客户端 case GEMINI: // 复用 OpenAI 客户端
case DOU_BAO: // 复用 OpenAI 客户端
case HUN_YUAN: // 复用 OpenAI 客户端
case XING_HUO: // 复用 OpenAI 客户端
case SILICON_FLOW: // 复用 OpenAI 客户端
case BAI_CHUAN: // 复用 OpenAI 客户端 case BAI_CHUAN: // 复用 OpenAI 客户端
return OpenAiChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens) return OpenAiChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens)
.toolNames(toolNames).toolContext(toolContext).build(); .toolCallbacks(toolCallbacks).toolContext(toolContext).build();
case AZURE_OPENAI: case AZURE_OPENAI:
return AzureOpenAiChatOptions.builder().deploymentName(model).temperature(temperature).maxTokens(maxTokens) return AzureOpenAiChatOptions.builder().deploymentName(model).temperature(temperature).maxTokens(maxTokens)
.toolNames(toolNames).toolContext(toolContext).build(); .toolCallbacks(toolCallbacks).toolContext(toolContext).build();
case ANTHROPIC:
return AnthropicChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens)
.toolCallbacks(toolCallbacks).toolContext(toolContext).build();
case OLLAMA: case OLLAMA:
return OllamaOptions.builder().model(model).temperature(temperature).numPredict(maxTokens) return OllamaOptions.builder().model(model).temperature(temperature).numPredict(maxTokens)
.toolNames(toolNames).toolContext(toolContext).build(); .toolCallbacks(toolCallbacks).toolContext(toolContext).build();
default: default:
throw new IllegalArgumentException(StrUtil.format("未知平台({})", platform)); throw new IllegalArgumentException(StrUtil.format("未知平台({})", platform));
} }
@ -98,4 +107,27 @@ public class AiUtils {
return context; return context;
} }
@SuppressWarnings("ConstantValue")
public static String getChatResponseContent(ChatResponse response) {
if (response == null
|| response.getResult() == null
|| response.getResult().getOutput() == null) {
return null;
}
return response.getResult().getOutput().getText();
}
@SuppressWarnings("ConstantValue")
public static String getChatResponseReasoningContent(ChatResponse response) {
if (response == null
|| response.getResult() == null
|| response.getResult().getOutput() == null) {
return null;
}
if (response.getResult().getOutput() instanceof DeepSeekAssistantMessage) {
return ((DeepSeekAssistantMessage) (response.getResult().getOutput())).getReasoningContent();
}
return null;
}
} }

View File

@ -0,0 +1,37 @@
package cn.iocoder.yudao.module.ai.util;
import cn.hutool.core.util.StrUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.tika.Tika;
/**
* 文件类型 Utils
*
* @author 芋道源码
*/
@Slf4j
public class FileTypeUtils {
private static final Tika TIKA = new Tika();
/**
* 已知文件名获取文件类型在某些情况下比通过字节数组准确例如使用 jar 文件时通过名字更为准确
*
* @param name 文件名
* @return mineType 无法识别时会返回application/octet-stream
*/
public static String getMineType(String name) {
return TIKA.detect(name);
}
/**
* 判断是否是图片
*
* @param mineType 类型
* @return 是否是图片
*/
public static boolean isImage(String mineType) {
return StrUtil.startWith(mineType, "image/");
}
}

View File

@ -132,7 +132,8 @@ spring:
azure: # OpenAI 微软 azure: # OpenAI 微软
openai: openai:
endpoint: https://eastusprejade.openai.azure.com endpoint: https://eastusprejade.openai.azure.com
api-key: xxx anthropic: # Anthropic Claude
api-key: sk-muubv7cXeLw0Etgs743f365cD5Ea44429946Fa7e672d8942
ollama: ollama:
base-url: http://127.0.0.1:11434 base-url: http://127.0.0.1:11434
chat: chat:
@ -140,7 +141,7 @@ spring:
stabilityai: stabilityai:
api-key: sk-e53UqbboF8QJCscYvzJscJxJXoFcFg4iJjl1oqgE7baJETmx api-key: sk-e53UqbboF8QJCscYvzJscJxJXoFcFg4iJjl1oqgE7baJETmx
dashscope: # 通义千问 dashscope: # 通义千问
api-key: sk-71800982914041848008480000000000 api-key: sk-47aa124781be4bfb95244cc62f6xxxx
minimax: # Minimaxhttps://www.minimaxi.com/ minimax: # Minimaxhttps://www.minimaxi.com/
api-key: xxxx api-key: xxxx
moonshot: # 月之暗灭KIMI moonshot: # 月之暗灭KIMI
@ -150,9 +151,30 @@ spring:
chat: chat:
options: options:
model: deepseek-chat model: deepseek-chat
model:
rerank: false # 是否开启“通义千问”的 Rerank 模型,填写 dashscope 开启
mcp:
server:
enabled: true
name: yudao-mcp-server
version: 1.0.0
instructions: 一个 MCP 示例服务
sse-endpoint: /sse
client:
enabled: true
name: mcp
sse:
connections:
filesystem:
url: http://127.0.0.1:8089
sse-endpoint: /sse
yudao: yudao:
ai: ai:
gemini: # 谷歌 Gemini
enable: true
api-key: AIzaSyAVoBxgoFvvte820vEQMma2LKBnC98bqMQ
model: gemini-2.5-flash
doubao: # 字节豆包 doubao: # 字节豆包
enable: true enable: true
api-key: 5c1b5747-26d2-4ebd-a4e0-dd0e8d8b4272 api-key: 5c1b5747-26d2-4ebd-a4e0-dd0e8d8b4272
@ -169,7 +191,7 @@ yudao:
enable: true enable: true
appKey: 75b161ed2aef4719b275d6e7f2a4d4cd appKey: 75b161ed2aef4719b275d6e7f2a4d4cd
secretKey: YWYxYWI2MTA4ODI2NGZlYTQyNjAzZTcz secretKey: YWYxYWI2MTA4ODI2NGZlYTQyNjAzZTcz
model: generalv3.5 model: x1
baichuan: # 百川智能 baichuan: # 百川智能
enable: true enable: true
api-key: sk-abc api-key: sk-abc
@ -184,6 +206,9 @@ yudao:
enable: true enable: true
# base-url: https://suno-55ishh05u-status2xxs-projects.vercel.app # base-url: https://suno-55ishh05u-status2xxs-projects.vercel.app
base-url: http://127.0.0.1:3001 base-url: http://127.0.0.1:3001
web-search:
enable: true
api-key: sk-40500e52840f4d24b956d0b1d80d9abe
--- #################### 芋道相关配置 #################### --- #################### 芋道相关配置 ####################

View File

@ -0,0 +1,87 @@
package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat;
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;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt;
import reactor.core.publisher.Flux;
import java.util.ArrayList;
import java.util.List;
/**
* {@link AnthropicChatModel} 集成测试类
*
* @author 芋道源码
*/
public class AnthropicChatModelTest {
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)
.temperature(0.7)
.maxTokens(4096)
.build())
.build();
@Test
@Disabled
public void testCall() {
// 准备参数
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);
}
@Test
@Disabled
public void testStream() {
// 准备参数
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(System.out::println).then().block();
}
// TODO @芋艿需要等 spring ai 升级https://github.com/spring-projects/spring-ai/pull/2800
@Test
@Disabled
public void testStream_thinking() {
// 准备参数
List<Message> messages = new ArrayList<>();
messages.add(new UserMessage("thkinking 下1+1 为什么等于 2 "));
AnthropicChatOptions options = AnthropicChatOptions.builder()
.model(AnthropicApi.ChatModel.CLAUDE_SONNET_4)
.thinking(AnthropicApi.ThinkingType.ENABLED, 3096)
.temperature(1D)
.build();
// 调用
Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages, options));
// 打印结果
flux.doOnNext(response -> {
// System.out.println(response);
System.out.println(response.getResult());
}).then().block();
}
}

View File

@ -60,4 +60,23 @@ public class DeepSeekChatModelTests {
flux.doOnNext(System.out::println).then().block(); flux.doOnNext(System.out::println).then().block();
} }
@Test
@Disabled
public void testStream_thinking() {
// 准备参数
List<Message> messages = new ArrayList<>();
messages.add(new UserMessage("详细分析下,如何设计一个电商系统?"));
DeepSeekChatOptions options = DeepSeekChatOptions.builder()
.model("deepseek-reasoner")
.build();
// 调用
Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages, options));
// 打印结果
flux.doOnNext(response -> {
// System.out.println(response);
System.out.println(response.getResult().getOutput());
}).then().block();
}
} }

View File

@ -8,9 +8,9 @@ import org.springframework.ai.chat.messages.SystemMessage;
import org.springframework.ai.chat.messages.UserMessage; import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt; import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.openai.OpenAiChatModel; import org.springframework.ai.deepseek.DeepSeekChatModel;
import org.springframework.ai.openai.OpenAiChatOptions; import org.springframework.ai.deepseek.DeepSeekChatOptions;
import org.springframework.ai.openai.api.OpenAiApi; import org.springframework.ai.deepseek.api.DeepSeekApi;
import reactor.core.publisher.Flux; import reactor.core.publisher.Flux;
import java.util.ArrayList; import java.util.ArrayList;
@ -23,13 +23,18 @@ import java.util.List;
*/ */
public class DouBaoChatModelTests { public class DouBaoChatModelTests {
private final OpenAiChatModel openAiChatModel = OpenAiChatModel.builder() /**
.openAiApi(OpenAiApi.builder() * 相比 OpenAIChatModel 来说DeepSeekChatModel 可以兼容豆包的 thinking 能力
*/
private final DeepSeekChatModel openAiChatModel = DeepSeekChatModel.builder()
.deepSeekApi(DeepSeekApi.builder()
.baseUrl(DouBaoChatModel.BASE_URL) .baseUrl(DouBaoChatModel.BASE_URL)
.completionsPath(DouBaoChatModel.COMPLETE_PATH)
.apiKey("5c1b5747-26d2-4ebd-a4e0-dd0e8d8b4272") // apiKey .apiKey("5c1b5747-26d2-4ebd-a4e0-dd0e8d8b4272") // apiKey
.build()) .build())
.defaultOptions(OpenAiChatOptions.builder() .defaultOptions(DeepSeekChatOptions.builder()
.model("doubao-1-5-lite-32k-250115") // 模型doubao .model("doubao-1-5-lite-32k-250115") // 模型doubao
// .model("doubao-seed-1-6-thinking-250715") // 模型doubao
// .model("deepseek-r1-250120") // 模型deepseek // .model("deepseek-r1-250120") // 模型deepseek
.temperature(0.7) .temperature(0.7)
.build()) .build())
@ -51,14 +56,13 @@ public class DouBaoChatModelTests {
System.out.println(response); System.out.println(response);
} }
// TODO @芋艿因为使用的是 v1 api导致 deepseek-r1-250120 不返回 think 过程后续需要优化
@Test @Test
@Disabled @Disabled
public void testStream() { public void testStream() {
// 准备参数 // 准备参数
List<Message> messages = new ArrayList<>(); List<Message> messages = new ArrayList<>();
messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
messages.add(new UserMessage("1 + 1 = ")); messages.add(new UserMessage("详细推理下,帮我设计一个用户中心!"));
// 调用 // 调用
Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages)); Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages));
@ -66,4 +70,23 @@ public class DouBaoChatModelTests {
flux.doOnNext(System.out::println).then().block(); flux.doOnNext(System.out::println).then().block();
} }
@Test
@Disabled
public void testStream_thinking() {
// 准备参数
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.doOnNext(response -> {
// System.out.println(response);
System.out.println(response.getResult().getOutput());
}).then().block();
}
} }

View File

@ -0,0 +1,68 @@
package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.gemini.GeminiChatModel;
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 org.springframework.ai.openai.api.OpenAiApi;
import reactor.core.publisher.Flux;
import java.util.ArrayList;
import java.util.List;
/**
* {@link GeminiChatModel} 集成测试
*
* @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 final GeminiChatModel chatModel = new GeminiChatModel(openAiChatModel);
@Test
@Disabled
public void testCall() {
// 准备参数
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);
}
@Test
@Disabled
public void testStream() {
// 准备参数
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(System.out::println).then().block();
}
}

View File

@ -8,9 +8,9 @@ import org.springframework.ai.chat.messages.SystemMessage;
import org.springframework.ai.chat.messages.UserMessage; import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt; import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.openai.OpenAiChatModel; import org.springframework.ai.deepseek.DeepSeekChatModel;
import org.springframework.ai.openai.OpenAiChatOptions; import org.springframework.ai.deepseek.DeepSeekChatOptions;
import org.springframework.ai.openai.api.OpenAiApi; import org.springframework.ai.deepseek.api.DeepSeekApi;
import reactor.core.publisher.Flux; import reactor.core.publisher.Flux;
import java.util.ArrayList; import java.util.ArrayList;
@ -23,12 +23,13 @@ import java.util.List;
*/ */
public class HunYuanChatModelTests { public class HunYuanChatModelTests {
private final OpenAiChatModel openAiChatModel = OpenAiChatModel.builder() private final DeepSeekChatModel openAiChatModel = DeepSeekChatModel.builder()
.openAiApi(OpenAiApi.builder() .deepSeekApi(DeepSeekApi.builder()
.baseUrl(HunYuanChatModel.BASE_URL) .baseUrl(HunYuanChatModel.BASE_URL)
.apiKey("sk-bcd") // apiKey .completionsPath(HunYuanChatModel.COMPLETE_PATH)
.apiKey("sk-abc") // apiKey
.build()) .build())
.defaultOptions(OpenAiChatOptions.builder() .defaultOptions(DeepSeekChatOptions.builder()
.model(HunYuanChatModel.MODEL_DEFAULT) // 模型 .model(HunYuanChatModel.MODEL_DEFAULT) // 模型
.temperature(0.7) .temperature(0.7)
.build()) .build())
@ -64,12 +65,33 @@ public class HunYuanChatModelTests {
flux.doOnNext(System.out::println).then().block(); flux.doOnNext(System.out::println).then().block();
} }
private final OpenAiChatModel deepSeekOpenAiChatModel = OpenAiChatModel.builder() @Test
.openAiApi(OpenAiApi.builder() @Disabled
public void testStream_thinking() {
// 准备参数
List<Message> messages = new ArrayList<>();
messages.add(new UserMessage("详细分析下,如何设计一个电商系统?"));
DeepSeekChatOptions options = DeepSeekChatOptions.builder()
.model("hunyuan-a13b")
// .model("hunyuan-turbos-latest")
.build();
// 调用
Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages, options));
// 打印结果
flux.doOnNext(response -> {
// System.out.println(response);
System.out.println(response.getResult().getOutput());
}).then().block();
}
private final DeepSeekChatModel deepSeekOpenAiChatModel = DeepSeekChatModel.builder()
.deepSeekApi(DeepSeekApi.builder()
.baseUrl(HunYuanChatModel.DEEP_SEEK_BASE_URL) .baseUrl(HunYuanChatModel.DEEP_SEEK_BASE_URL)
.completionsPath(HunYuanChatModel.COMPLETE_PATH)
.apiKey("sk-abc") // apiKey .apiKey("sk-abc") // apiKey
.build()) .build())
.defaultOptions(OpenAiChatOptions.builder() .defaultOptions(DeepSeekChatOptions.builder()
// .model(HunYuanChatModel.DEEP_SEEK_MODEL_DEFAULT) // 模型"deepseek-v3" // .model(HunYuanChatModel.DEEP_SEEK_MODEL_DEFAULT) // 模型"deepseek-v3"
.model("deepseek-r1") // 模型"deepseek-r1" .model("deepseek-r1") // 模型"deepseek-r1"
.temperature(0.7) .temperature(0.7)
@ -94,7 +116,7 @@ public class HunYuanChatModelTests {
@Test @Test
@Disabled @Disabled
public void testStream_deekseek() { public void testStream_deepseek() {
// 准备参数 // 准备参数
List<Message> messages = new ArrayList<>(); List<Message> messages = new ArrayList<>();
messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
@ -106,5 +128,23 @@ public class HunYuanChatModelTests {
flux.doOnNext(System.out::println).then().block(); 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

@ -1,6 +1,20 @@
package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat; package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat;
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.ollama.OllamaChatModel; import org.springframework.ai.ollama.OllamaChatModel;
import org.springframework.ai.ollama.api.OllamaApi;
import org.springframework.ai.ollama.api.OllamaModel;
import org.springframework.ai.ollama.api.OllamaOptions;
import reactor.core.publisher.Flux;
import java.util.ArrayList;
import java.util.List;
/** /**
* {@link OllamaChatModel} 集成测试 * {@link OllamaChatModel} 集成测试
@ -9,43 +23,65 @@ import org.springframework.ai.ollama.OllamaChatModel;
*/ */
public class LlamaChatModelTests { public class LlamaChatModelTests {
// private final OllamaChatModel chatModel = OllamaChatModel.builder() private final OllamaChatModel chatModel = OllamaChatModel.builder()
// .ollamaApi(new OllamaApi("http://127.0.0.1:11434")) // Ollama 服务地址 .ollamaApi(OllamaApi.builder()
// .defaultOptions(OllamaOptions.builder() .baseUrl("http://127.0.0.1:11434") // Ollama 服务地址
// .model(OllamaModel.LLAMA3.getName()) // 模型 .build())
// .build()) .defaultOptions(OllamaOptions.builder()
// .build(); .model(OllamaModel.LLAMA3.getName()) // 模型
// .build())
// @Test .build();
// @Disabled
// public void testCall() { @Test
// // 准备参数 @Disabled
// List<Message> messages = new ArrayList<>(); public void testCall() {
// messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); // 准备参数
// messages.add(new UserMessage("1 + 1 = ")); 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); ChatResponse response = chatModel.call(new Prompt(messages));
// System.out.println(response.getResult().getOutput()); // 打印结果
// } System.out.println(response);
// System.out.println(response.getResult().getOutput());
// @Test }
// @Disabled
// public void testStream() { @Test
// // 准备参数 @Disabled
// List<Message> messages = new ArrayList<>(); public void testStream() {
// messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); // 准备参数
// messages.add(new UserMessage("1 + 1 = ")); 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 -> { Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages));
//// System.out.println(response); // 打印结果
// System.out.println(response.getResult().getOutput()); flux.doOnNext(response -> {
// }).then().block(); // System.out.println(response);
// } System.out.println(response.getResult().getOutput());
}).then().block();
}
@Test
@Disabled
public void testStream_thinking() {
// 准备参数
List<Message> messages = new ArrayList<>();
messages.add(new UserMessage("详细分析下,如何设计一个电商系统?"));
OllamaOptions options = OllamaOptions.builder()
.model("qwen3")
.build();
// 调用
Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages, options));
// 打印结果
flux.doOnNext(response -> {
// System.out.println(response);
System.out.println(response.getResult().getOutput());
}).then().block();
}
} }

View File

@ -59,4 +59,24 @@ public class MiniMaxChatModelTests {
}).then().block(); }).then().block();
} }
// TODO @芋艿暂时没解析 reasoning_content 结果需要等官方修复
@Test
@Disabled
public void testStream_thinking() {
// 准备参数
List<Message> messages = new ArrayList<>();
messages.add(new UserMessage("详细分析下,如何设计一个电商系统?"));
MiniMaxChatOptions options = MiniMaxChatOptions.builder()
.model("MiniMax-M1")
.build();
// 调用
Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages, options));
// 打印结果
flux.doOnNext(response -> {
// System.out.println(response);
System.out.println(response.getResult().getOutput());
}).then().block();
}
} }

View File

@ -63,4 +63,25 @@ public class MoonshotChatModelTests {
}).then().block(); }).then().block();
} }
// TODO @芋艿暂时没解析 reasoning_content 结果需要等官方修复
@Test
@Disabled
public void testStream_thinking() {
// 准备参数
List<Message> messages = new ArrayList<>();
messages.add(new UserMessage("详细分析下,如何设计一个电商系统?"));
MoonshotChatOptions options = MoonshotChatOptions.builder()
// .model("kimi-k2-0711-preview")
.model("kimi-thinking-preview")
.build();
// 调用
Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages, options));
// 打印结果
flux.doOnNext(response -> {
// System.out.println(response);
System.out.println(response.getResult().getOutput());
}).then().block();
}
} }

View File

@ -1,5 +1,6 @@
package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat; package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat;
import com.azure.ai.openai.models.ReasoningEffortValue;
import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.ai.chat.messages.Message; import org.springframework.ai.chat.messages.Message;
@ -25,10 +26,11 @@ public class OpenAIChatModelTests {
private final OpenAiChatModel chatModel = OpenAiChatModel.builder() private final OpenAiChatModel chatModel = OpenAiChatModel.builder()
.openAiApi(OpenAiApi.builder() .openAiApi(OpenAiApi.builder()
.baseUrl("https://api.holdai.top") .baseUrl("https://api.holdai.top")
.apiKey("sk-PytRecQlmjEteoa2RRN6cGnwslo72UUPLQVNEMS6K9yjbmpD") // apiKey .apiKey("sk-z5joyRoV1iFEnh2SAi8QPNrIZTXyQSyxTmD5CoNDQbFixK2l") // apiKey
.build()) .build())
.defaultOptions(OpenAiChatOptions.builder() .defaultOptions(OpenAiChatOptions.builder()
.model(OpenAiApi.ChatModel.GPT_4_1_NANO) // 模型 .model("gpt-5-nano-2025-08-07") // 模型
// .model(OpenAiApi.ChatModel.O1) // 模型
.temperature(0.7) .temperature(0.7)
.build()) .build())
.build(); .build();
@ -54,7 +56,7 @@ public class OpenAIChatModelTests {
// 准备参数 // 准备参数
List<Message> messages = new ArrayList<>(); List<Message> messages = new ArrayList<>();
messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
messages.add(new UserMessage("1 + 1 = ")); messages.add(new UserMessage("帮我推理下,怎么实现一个用户中心!"));
// 调用 // 调用
Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages)); Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages));
@ -65,4 +67,29 @@ public class OpenAIChatModelTests {
}).then().block(); }).then().block();
} }
// TODO @芋艿无法触发思考的字段返回需要 response apihttps://github.com/spring-projects/spring-ai/issues/2962
@Test
@Disabled
public void testStream_thinking() {
// 准备参数
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())
.build();
// 调用
Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages, options));
// 打印结果
flux.doOnNext(response -> {
// System.out.println(response);
System.out.println(response.getResult().getOutput());
}).then().block();
}
} }

View File

@ -9,9 +9,9 @@ import org.springframework.ai.chat.messages.SystemMessage;
import org.springframework.ai.chat.messages.UserMessage; import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt; import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.openai.OpenAiChatModel; import org.springframework.ai.deepseek.DeepSeekChatModel;
import org.springframework.ai.openai.OpenAiChatOptions; import org.springframework.ai.deepseek.DeepSeekChatOptions;
import org.springframework.ai.openai.api.OpenAiApi; import org.springframework.ai.deepseek.api.DeepSeekApi;
import reactor.core.publisher.Flux; import reactor.core.publisher.Flux;
import java.util.ArrayList; import java.util.ArrayList;
@ -24,12 +24,12 @@ import java.util.List;
*/ */
public class SiliconFlowChatModelTests { public class SiliconFlowChatModelTests {
private final OpenAiChatModel openAiChatModel = OpenAiChatModel.builder() private final DeepSeekChatModel openAiChatModel = DeepSeekChatModel.builder()
.openAiApi(OpenAiApi.builder() .deepSeekApi(DeepSeekApi.builder()
.baseUrl(SiliconFlowApiConstants.DEFAULT_BASE_URL) .baseUrl(SiliconFlowApiConstants.DEFAULT_BASE_URL)
.apiKey("sk-epsakfenqnyzoxhmbucsxlhkdqlcbnimslqoivkshalvdozz") // apiKey .apiKey("sk-epsakfenqnyzoxhmbucsxlhkdqlcbnimslqoivkshalvdozz") // apiKey
.build()) .build())
.defaultOptions(OpenAiChatOptions.builder() .defaultOptions(DeepSeekChatOptions.builder()
.model(SiliconFlowApiConstants.MODEL_DEFAULT) // 模型 .model(SiliconFlowApiConstants.MODEL_DEFAULT) // 模型
// .model("deepseek-ai/DeepSeek-R1") // 模型deepseek-ai/DeepSeek-R1可用赠费 // .model("deepseek-ai/DeepSeek-R1") // 模型deepseek-ai/DeepSeek-R1可用赠费
// .model("Pro/deepseek-ai/DeepSeek-R1") // 模型Pro/deepseek-ai/DeepSeek-R1需要付费 // .model("Pro/deepseek-ai/DeepSeek-R1") // 模型Pro/deepseek-ai/DeepSeek-R1需要付费
@ -67,4 +67,23 @@ public class SiliconFlowChatModelTests {
flux.doOnNext(System.out::println).then().block(); flux.doOnNext(System.out::println).then().block();
} }
@Test
@Disabled
public void testStream_thinking() {
// 准备参数
List<Message> messages = new ArrayList<>();
messages.add(new UserMessage("详细分析下,如何设计一个电商系统?"));
DeepSeekChatOptions options = DeepSeekChatOptions.builder()
.model("deepseek-ai/DeepSeek-R1")
.build();
// 调用
Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages, options));
// 打印结果
flux.doOnNext(response -> {
// System.out.println(response);
System.out.println(response.getResult().getOutput());
}).then().block();
}
} }

View File

@ -1,8 +1,15 @@
package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat; package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import com.alibaba.cloud.ai.dashscope.api.DashScopeApi; import com.alibaba.cloud.ai.dashscope.api.DashScopeApi;
import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel; import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel;
import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatOptions; import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatOptions;
import com.alibaba.cloud.ai.dashscope.rerank.DashScopeRerankModel;
import com.alibaba.cloud.ai.dashscope.rerank.DashScopeRerankOptions;
import com.alibaba.cloud.ai.model.RerankModel;
import com.alibaba.cloud.ai.model.RerankOptions;
import com.alibaba.cloud.ai.model.RerankRequest;
import com.alibaba.cloud.ai.model.RerankResponse;
import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.ai.chat.messages.Message; import org.springframework.ai.chat.messages.Message;
@ -10,11 +17,14 @@ import org.springframework.ai.chat.messages.SystemMessage;
import org.springframework.ai.chat.messages.UserMessage; import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt; import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.document.Document;
import reactor.core.publisher.Flux; import reactor.core.publisher.Flux;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import static java.util.Arrays.asList;
/** /**
* {@link DashScopeChatModel} 集成测试类 * {@link DashScopeChatModel} 集成测试类
* *
@ -26,11 +36,13 @@ public class TongYiChatModelTests {
.dashScopeApi(DashScopeApi.builder() .dashScopeApi(DashScopeApi.builder()
.apiKey("sk-47aa124781be4bfb95244cc62f63f7d0") .apiKey("sk-47aa124781be4bfb95244cc62f63f7d0")
.build()) .build())
.defaultOptions( DashScopeChatOptions.builder() .defaultOptions(DashScopeChatOptions.builder()
.withModel("qwen1.5-72b-chat") // 模型 // .withModel("qwen1.5-72b-chat") // 模型
.withModel("qwen3-235b-a22b-thinking-2507") // 模型
// .withModel("deepseek-r1") // 模型deepseek-r1 // .withModel("deepseek-r1") // 模型deepseek-r1
// .withModel("deepseek-v3") // 模型deepseek-v3 // .withModel("deepseek-v3") // 模型deepseek-v3
// .withModel("deepseek-r1-distill-qwen-1.5b") // 模型deepseek-r1-distill-qwen-1.5b // .withModel("deepseek-r1-distill-qwen-1.5b") // 模型deepseek-r1-distill-qwen-1.5b
// .withEnableThinking(true)
.build()) .build())
.build(); .build();
@ -54,8 +66,8 @@ public class TongYiChatModelTests {
public void testStream() { public void testStream() {
// 准备参数 // 准备参数
List<Message> messages = new ArrayList<>(); List<Message> messages = new ArrayList<>();
messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); // messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
messages.add(new UserMessage("1 + 1 = ")); messages.add(new UserMessage("帮我推理下,怎么实现一个用户中心!"));
// 调用 // 调用
Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages)); Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages));
@ -66,4 +78,52 @@ public class TongYiChatModelTests {
}).then().block(); }).then().block();
} }
@Test
@Disabled
public void testStream_thinking() {
// 准备参数
List<Message> messages = new ArrayList<>();
messages.add(new UserMessage("详细分析下,如何设计一个电商系统?"));
DashScopeChatOptions options = DashScopeChatOptions.builder()
.withModel("qwen3-235b-a22b-thinking-2507")
// .withModel("qwen-max-2025-01-25")
.withEnableThinking(true) // 必须设置否则会报错
.build();
// 调用
Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages, options));
// 打印结果
flux.doOnNext(response -> {
// System.out.println(response);
System.out.println(response.getResult().getOutput());
}).then().block();
}
@Test
@Disabled
public void testRerank() {
// 准备环境
RerankModel rerankModel = new DashScopeRerankModel(
DashScopeApi.builder()
.apiKey("sk-47aa124781be4bfb95244cc62f63f7d0")
.build());
// 准备参数
String query = "spring";
Document document01 = new Document("abc");
Document document02 = new Document("sapring");
RerankOptions options = DashScopeRerankOptions.builder()
.withTopN(1)
.withModel("gte-rerank-v2")
.build();
RerankRequest rerankRequest = new RerankRequest(
query,
asList(document01, document02),
options);
// 调用
RerankResponse call = rerankModel.call(rerankRequest);
// 打印结果
System.out.println(JsonUtils.toJsonPrettyString(call));
}
} }

View File

@ -8,9 +8,9 @@ import org.springframework.ai.chat.messages.SystemMessage;
import org.springframework.ai.chat.messages.UserMessage; import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt; import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.openai.OpenAiChatModel; import org.springframework.ai.deepseek.DeepSeekChatModel;
import org.springframework.ai.openai.OpenAiChatOptions; import org.springframework.ai.deepseek.DeepSeekChatOptions;
import org.springframework.ai.openai.api.OpenAiApi; import org.springframework.ai.deepseek.api.DeepSeekApi;
import reactor.core.publisher.Flux; import reactor.core.publisher.Flux;
import java.util.ArrayList; import java.util.ArrayList;
@ -23,13 +23,15 @@ import java.util.List;
*/ */
public class XingHuoChatModelTests { public class XingHuoChatModelTests {
private final OpenAiChatModel openAiChatModel = OpenAiChatModel.builder() private final DeepSeekChatModel openAiChatModel = DeepSeekChatModel.builder()
.openAiApi(OpenAiApi.builder() .deepSeekApi(DeepSeekApi.builder()
.baseUrl(XingHuoChatModel.BASE_URL) .baseUrl(XingHuoChatModel.BASE_URL_V2)
.completionsPath(XingHuoChatModel.BASE_COMPLETIONS_PATH_V2)
.apiKey("75b161ed2aef4719b275d6e7f2a4d4cd:YWYxYWI2MTA4ODI2NGZlYTQyNjAzZTcz") // appKey:secretKey .apiKey("75b161ed2aef4719b275d6e7f2a4d4cd:YWYxYWI2MTA4ODI2NGZlYTQyNjAzZTcz") // appKey:secretKey
.build()) .build())
.defaultOptions(OpenAiChatOptions.builder() .defaultOptions(DeepSeekChatOptions.builder()
.model("generalv3.5") // 模型 // .model("generalv3.5") // 模型
.model("x1") // 模型
.temperature(0.7) .temperature(0.7)
.build()) .build())
.build(); .build();
@ -64,4 +66,23 @@ public class XingHuoChatModelTests {
flux.doOnNext(System.out::println).then().block(); flux.doOnNext(System.out::println).then().block();
} }
@Test
@Disabled
public void testStream_thinking() {
// 准备参数
List<Message> messages = new ArrayList<>();
messages.add(new UserMessage("详细分析下,如何设计一个电商系统?"));
DeepSeekChatOptions options = DeepSeekChatOptions.builder()
.model("x1")
.build();
// 调用
Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages, options));
// 打印结果
flux.doOnNext(response -> {
// System.out.println(response);
System.out.println(response.getResult().getOutput());
}).then().block();
}
} }

View File

@ -23,7 +23,7 @@ import java.util.List;
public class ZhiPuAiChatModelTests { public class ZhiPuAiChatModelTests {
private final ZhiPuAiChatModel chatModel = new ZhiPuAiChatModel( private final ZhiPuAiChatModel chatModel = new ZhiPuAiChatModel(
new ZhiPuAiApi("32f84543e54eee31f8d56b2bd6020573.3vh9idLJZ2ZhxDEs"), // 密钥 new ZhiPuAiApi("2f35fb6ca4ea41fab898729b7fac086c.6ESSfPcCkxaKEUlR"), // 密钥
ZhiPuAiChatOptions.builder() ZhiPuAiChatOptions.builder()
.model(ZhiPuAiApi.ChatModel.GLM_4.getName()) // 模型 .model(ZhiPuAiApi.ChatModel.GLM_4.getName()) // 模型
.build() .build()
@ -61,4 +61,24 @@ public class ZhiPuAiChatModelTests {
}).then().block(); }).then().block();
} }
// TODO @芋艿暂时没解析 reasoning_content 结果需要等官方修复
@Test
@Disabled
public void testStream_thinking() {
// 准备参数
List<Message> messages = new ArrayList<>();
messages.add(new UserMessage("详细分析下,如何设计一个电商系统?"));
ZhiPuAiChatOptions options = ZhiPuAiChatOptions.builder()
.model("GLM-4.5")
.build();
// 调用
Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages, options));
// 打印结果
flux.doOnNext(response -> {
// System.out.println(response);
System.out.println(response.getResult().getOutput());
}).then().block();
}
} }

View File

@ -16,8 +16,11 @@ import org.springframework.ai.image.ImageResponse;
*/ */
public class TongYiImagesModelTest { public class TongYiImagesModelTest {
private final DashScopeImageModel imageModel = new DashScopeImageModel( private final DashScopeImageModel imageModel = DashScopeImageModel.builder()
new DashScopeImageApi("sk-7d903764249848cfa912733146da12d1")); .dashScopeApi(DashScopeImageApi.builder()
.apiKey("sk-47aa124781be4bfb95244cc62f63f7d0")
.build())
.build();
@Test @Test
@Disabled @Disabled

View File

@ -0,0 +1,28 @@
package cn.iocoder.yudao.module.ai.framework.ai.core.websearch;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.AiWebSearchRequest;
import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.AiWebSearchResponse;
import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.bocha.AiBoChaWebSearchClient;
import org.junit.jupiter.api.Test;
/**
* {@link AiBoChaWebSearchClient} 集成测试类
*
* @author 芋道源码
*/
public class AiBoChaWebSearchClientTest {
private final AiBoChaWebSearchClient webSearchClient = new AiBoChaWebSearchClient(
"sk-40500e52840f4d24b956d0b1d80d9abe");
@Test
public void testSearch() {
AiWebSearchRequest request = new AiWebSearchRequest()
.setQuery("阿里巴巴")
.setCount(3);
AiWebSearchResponse response = webSearchClient.search(request);
System.out.println(JsonUtils.toJsonPrettyString(response));
}
}

View File

@ -35,7 +35,7 @@ public enum BpmSimpleModelNodeTypeEnum implements ArrayValuable<Integer> {
// 50 ~ 条件分支 // 50 ~ 条件分支
CONDITION_NODE(50, "条件", "sequenceFlow"), // 用于构建流转条件的表达式 CONDITION_NODE(50, "条件", "sequenceFlow"), // 用于构建流转条件的表达式
CONDITION_BRANCH_NODE(51, "条件分支", "exclusiveGateway"), CONDITION_BRANCH_NODE(51, "条件分支", "exclusiveGateway"),
PARALLEL_BRANCH_NODE(52, "并行分支", "parallelGateway"), PARALLEL_BRANCH_NODE(52, "并行分支", "inclusiveGateway"), // 并行分支使用包容网关实现条件表达式结果设置为 true
INCLUSIVE_BRANCH_NODE(53, "包容分支", "inclusiveGateway"), INCLUSIVE_BRANCH_NODE(53, "包容分支", "inclusiveGateway"),
ROUTER_BRANCH_NODE(54, "路由分支", "exclusiveGateway") ROUTER_BRANCH_NODE(54, "路由分支", "exclusiveGateway")
; ;

View File

@ -80,17 +80,17 @@ public class FileTypeUtils {
*/ */
public static void writeAttachment(HttpServletResponse response, String filename, byte[] content) throws IOException { public static void writeAttachment(HttpServletResponse response, String filename, byte[] content) throws IOException {
// 设置 header contentType // 设置 header contentType
String contentType = getMineType(content, filename); String mineType = getMineType(content, filename);
response.setContentType(contentType); response.setContentType(mineType);
// 设置内容显示下载文件名https://www.cnblogs.com/wq-9/articles/12165056.html // 设置内容显示下载文件名https://www.cnblogs.com/wq-9/articles/12165056.html
if (StrUtil.containsIgnoreCase(contentType, "image/")) { if (isImage(mineType)) {
// 参见 https://github.com/YunaiV/ruoyi-vue-pro/issues/692 讨论 // 参见 https://github.com/YunaiV/ruoyi-vue-pro/issues/692 讨论
response.setHeader("Content-Disposition", "inline;filename=" + HttpUtils.encodeUtf8(filename)); response.setHeader("Content-Disposition", "inline;filename=" + HttpUtils.encodeUtf8(filename));
} else { } else {
response.setHeader("Content-Disposition", "attachment;filename=" + HttpUtils.encodeUtf8(filename)); response.setHeader("Content-Disposition", "attachment;filename=" + HttpUtils.encodeUtf8(filename));
} }
// 针对 video 的特殊处理解决视频地址在移动端播放的兼容性问题 // 针对 video 的特殊处理解决视频地址在移动端播放的兼容性问题
if (StrUtil.containsIgnoreCase(contentType, "video")) { if (StrUtil.containsIgnoreCase(mineType, "video")) {
response.setHeader("Content-Length", String.valueOf(content.length)); response.setHeader("Content-Length", String.valueOf(content.length));
response.setHeader("Content-Range", "bytes 0-" + (content.length - 1) + "/" + content.length); response.setHeader("Content-Range", "bytes 0-" + (content.length - 1) + "/" + content.length);
response.setHeader("Accept-Ranges", "bytes"); response.setHeader("Accept-Ranges", "bytes");
@ -99,4 +99,14 @@ public class FileTypeUtils {
IoUtil.write(response.getOutputStream(), false, content); IoUtil.write(response.getOutputStream(), false, content);
} }
/**
* 判断是否是图片
*
* @param mineType 类型
* @return 是否是图片
*/
public static boolean isImage(String mineType) {
return StrUtil.startWith(mineType, "image/");
}
} }

View File

@ -166,6 +166,10 @@ public class CouponServiceImpl implements CouponService {
public void invalidateCouponsByAdmin(List<Long> giveCouponIds, Long userId) { public void invalidateCouponsByAdmin(List<Long> giveCouponIds, Long userId) {
// 循环收回 // 循环收回
for (Long couponId : giveCouponIds) { for (Long couponId : giveCouponIds) {
// couponId 为空或 0 则跳过
if (null == couponId || couponId <= 0) {
continue;
}
try { try {
getSelf().invalidateCoupon(couponId, userId); getSelf().invalidateCoupon(couponId, userId);
} catch (Exception e) { } catch (Exception e) {

View File

@ -541,14 +541,23 @@ public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientC
* @see <a href="https://github.com/binarywang/weixin-java-pay-demo/blob/master/src/main/java/com/github/binarywang/demo/wx/pay/controller/WxPayV3Controller.java#L202-L221">官方示例</a> * @see <a href="https://github.com/binarywang/weixin-java-pay-demo/blob/master/src/main/java/com/github/binarywang/demo/wx/pay/controller/WxPayV3Controller.java#L202-L221">官方示例</a>
*/ */
private SignatureHeader getRequestHeader(Map<String, String> headers) { private SignatureHeader getRequestHeader(Map<String, String> headers) {
// 参见 https://gitee.com/zhijiantianya/yudao-cloud/issues/ICSFL6
return SignatureHeader.builder() return SignatureHeader.builder()
.signature(headers.get("wechatpay-signature")) .signature(getHeaderValue(headers, "Wechatpay-Signature", "wechatpay-signature"))
.nonce(headers.get("wechatpay-nonce")) .nonce(getHeaderValue(headers, "Wechatpay-Nonce", "wechatpay-nonce"))
.serial(headers.get("wechatpay-serial")) .serial(getHeaderValue(headers, "Wechatpay-Serial", "wechatpay-serial"))
.timeStamp(headers.get("wechatpay-timestamp")) .timeStamp(getHeaderValue(headers, "Wechatpay-Timestamp", "wechatpay-timestamp"))
.build(); .build();
} }
private String getHeaderValue(Map<String, String> headers, String capitalizedKey, String lowercaseKey) {
String value = headers.get(capitalizedKey);
if (value != null) {
return value;
}
return headers.get(lowercaseKey);
}
// TODO @芋艿可能是 wxjava bughttps://github.com/binarywang/WxJava/issues/1557 // TODO @芋艿可能是 wxjava bughttps://github.com/binarywang/WxJava/issues/1557
private void fixV3HttpClientConnectionPoolShutDown() { private void fixV3HttpClientConnectionPoolShutDown() {
client.getConfig().setApiV3HttpClient(null); client.getConfig().setApiV3HttpClient(null);

View File

@ -1,6 +1,5 @@
package cn.iocoder.yudao.module.system.framework.captcha.core; package cn.iocoder.yudao.module.system.framework.captcha.core;
import cn.hutool.core.util.RandomUtil;
import com.anji.captcha.model.common.RepCodeEnum; import com.anji.captcha.model.common.RepCodeEnum;
import com.anji.captcha.model.common.ResponseModel; import com.anji.captcha.model.common.ResponseModel;
import com.anji.captcha.model.vo.CaptchaVO; import com.anji.captcha.model.vo.CaptchaVO;
@ -9,7 +8,8 @@ import com.anji.captcha.service.impl.CaptchaServiceFactory;
import com.anji.captcha.util.AESUtil; import com.anji.captcha.util.AESUtil;
import com.anji.captcha.util.ImageUtils; import com.anji.captcha.util.ImageUtils;
import com.anji.captcha.util.RandomUtils; import com.anji.captcha.util.RandomUtils;
import org.apache.commons.lang3.StringUtils; import cn.hutool.core.util.RandomUtil;
import org.apache.commons.lang3.Strings;
import java.awt.*; import java.awt.*;
import java.awt.geom.AffineTransform; import java.awt.geom.AffineTransform;
@ -82,7 +82,7 @@ public class PictureWordCaptchaServiceImpl extends AbstractCaptchaService {
// 用户输入的验证码(CaptchaVO 没有预留字段暂时用 pointJson 无需加解密) // 用户输入的验证码(CaptchaVO 没有预留字段暂时用 pointJson 无需加解密)
String userCode = captchaVO.getPointJson(); String userCode = captchaVO.getPointJson();
if (!StringUtils.equalsIgnoreCase(code, userCode)) { if (!Strings.CI.equals(code, userCode)) {
afterValidateFail(captchaVO); afterValidateFail(captchaVO);
return ResponseModel.errorMsg(RepCodeEnum.API_CAPTCHA_COORDINATE_ERROR); return ResponseModel.errorMsg(RepCodeEnum.API_CAPTCHA_COORDINATE_ERROR);
} }
@ -209,4 +209,4 @@ public class PictureWordCaptchaServiceImpl extends AbstractCaptchaService {
return RandomUtil.randomString(CHARACTERS, length); return RandomUtil.randomString(CHARACTERS, length);
} }
} }

View File

@ -78,7 +78,6 @@ public class AuthRequestFactory {
.keySet() .keySet()
.stream() .stream()
.filter(x -> names.contains(x.toUpperCase())) .filter(x -> names.contains(x.toUpperCase()))
.map(String::toUpperCase)
.collect(Collectors.toList()); .collect(Collectors.toList());
} }
@ -318,4 +317,4 @@ public class AuthRequestFactory {
.proxy(new Proxy(Proxy.Type.valueOf(proxyConfig.getType()), new InetSocketAddress(proxyConfig.getHostname(), proxyConfig.getPort()))) .proxy(new Proxy(Proxy.Type.valueOf(proxyConfig.getType()), new InetSocketAddress(proxyConfig.getHostname(), proxyConfig.getPort())))
.build()); .build());
} }
} }

View File

@ -111,6 +111,7 @@ public class AliyunSmsClient extends AbstractSmsClient {
} }
@VisibleForTesting @VisibleForTesting
@SuppressWarnings("EnhancedSwitchMigration")
Integer convertSmsTemplateAuditStatus(Integer templateStatus) { Integer convertSmsTemplateAuditStatus(Integer templateStatus) {
switch (templateStatus) { switch (templateStatus) {
case 0: return SmsTemplateAuditStatusEnum.CHECKING.getStatus(); case 0: return SmsTemplateAuditStatusEnum.CHECKING.getStatus();
@ -135,7 +136,7 @@ public class AliyunSmsClient extends AbstractSmsClient {
.collect(Collectors.joining("&")); .collect(Collectors.joining("&"));
// 2. 请求 Body // 2. 请求 Body
String requestBody = ""; // 短信 API RPC 接口query parameters uri 中拼接因此 request body 如果没有特殊要求设置为空 String requestBody = ""; // 短信 API RPC 接口query parameters uri 中拼接因此 request body 如果没有特殊要求设置为空
String hashedRequestPayload = DigestUtil.sha256Hex(requestBody); String hashedRequestPayload = DigestUtil.sha256Hex(requestBody);
// 3.1 请求 Header // 3.1 请求 Header
@ -151,8 +152,8 @@ public class AliyunSmsClient extends AbstractSmsClient {
StringBuilder canonicalHeaders = new StringBuilder(); // 构造请求头多个规范化消息头按照消息头名称小写的字符代码顺序以升序排列后拼接在一起 StringBuilder canonicalHeaders = new StringBuilder(); // 构造请求头多个规范化消息头按照消息头名称小写的字符代码顺序以升序排列后拼接在一起
StringBuilder signedHeadersBuilder = new StringBuilder(); // 已签名消息头列表多个请求头名称小写按首字母升序排列并以英文分号;分隔 StringBuilder signedHeadersBuilder = new StringBuilder(); // 已签名消息头列表多个请求头名称小写按首字母升序排列并以英文分号;分隔
headers.entrySet().stream().filter(entry -> entry.getKey().toLowerCase().startsWith("x-acs-") headers.entrySet().stream().filter(entry -> entry.getKey().toLowerCase().startsWith("x-acs-")
|| entry.getKey().equalsIgnoreCase("host") || "host".equalsIgnoreCase(entry.getKey())
|| entry.getKey().equalsIgnoreCase("content-type")) || "content-type".equalsIgnoreCase(entry.getKey()))
.sorted(Map.Entry.comparingByKey()).forEach(entry -> { .sorted(Map.Entry.comparingByKey()).forEach(entry -> {
String lowerKey = entry.getKey().toLowerCase(); String lowerKey = entry.getKey().toLowerCase();
canonicalHeaders.append(lowerKey).append(":").append(String.valueOf(entry.getValue()).trim()).append("\n"); canonicalHeaders.append(lowerKey).append(":").append(String.valueOf(entry.getValue()).trim()).append("\n");
@ -193,4 +194,4 @@ public class AliyunSmsClient extends AbstractSmsClient {
.replace("%7E", "~"); // 波浪号 "%7E" 被替换为 "~" .replace("%7E", "~"); // 波浪号 "%7E" 被替换为 "~"
} }
} }

View File

@ -6,6 +6,7 @@ import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
import cn.iocoder.yudao.framework.common.util.monitor.TracerUtils; import cn.iocoder.yudao.framework.common.util.monitor.TracerUtils;
import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils; import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;
import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils; import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils;
import cn.iocoder.yudao.framework.datapermission.core.annotation.DataPermission;
import cn.iocoder.yudao.module.system.api.logger.dto.LoginLogCreateReqDTO; import cn.iocoder.yudao.module.system.api.logger.dto.LoginLogCreateReqDTO;
import cn.iocoder.yudao.module.system.api.sms.SmsCodeApi; import cn.iocoder.yudao.module.system.api.sms.SmsCodeApi;
import cn.iocoder.yudao.module.system.api.sms.dto.code.SmsCodeUseReqDTO; import cn.iocoder.yudao.module.system.api.sms.dto.code.SmsCodeUseReqDTO;
@ -98,6 +99,7 @@ public class AdminAuthServiceImpl implements AdminAuthService {
} }
@Override @Override
@DataPermission(enable = false)
public AuthLoginRespVO login(AuthLoginReqVO reqVO) { public AuthLoginRespVO login(AuthLoginReqVO reqVO) {
// 校验验证码 // 校验验证码
validateCaptcha(reqVO); validateCaptcha(reqVO);
@ -135,7 +137,7 @@ public class AdminAuthServiceImpl implements AdminAuthService {
@Override @Override
public AuthLoginRespVO smsLogin(AuthSmsLoginReqVO reqVO) { public AuthLoginRespVO smsLogin(AuthSmsLoginReqVO reqVO) {
// 校验验证码 // 校验验证码
smsCodeApi.useSmsCode(AuthConvert.INSTANCE.convert(reqVO, SmsSceneEnum.ADMIN_MEMBER_LOGIN.getScene(), getClientIP())).checkError(); smsCodeApi.useSmsCode(AuthConvert.INSTANCE.convert(reqVO, SmsSceneEnum.ADMIN_MEMBER_LOGIN.getScene(), getClientIP()));
// 获得用户信息 // 获得用户信息
AdminUserDO user = userService.getUserByMobile(reqVO.getMobile()); AdminUserDO user = userService.getUserByMobile(reqVO.getMobile());
@ -297,7 +299,7 @@ public class AdminAuthServiceImpl implements AdminAuthService {
.setMobile(reqVO.getMobile()) .setMobile(reqVO.getMobile())
.setScene(SmsSceneEnum.ADMIN_MEMBER_RESET_PASSWORD.getScene()) .setScene(SmsSceneEnum.ADMIN_MEMBER_RESET_PASSWORD.getScene())
.setUsedIp(getClientIP()) .setUsedIp(getClientIP())
).checkError(); );
userService.updateUserPassword(userByMobile.getId(), reqVO.getPassword()); userService.updateUserPassword(userByMobile.getId(), reqVO.getPassword());
} }

View File

@ -199,7 +199,8 @@ spring:
azure: # OpenAI 微软 azure: # OpenAI 微软
openai: openai:
endpoint: https://eastusprejade.openai.azure.com endpoint: https://eastusprejade.openai.azure.com
api-key: xxx anthropic: # Anthropic Claude
api-key: sk-muubv7cXeLw0Etgs743f365cD5Ea44429946Fa7e672d8942
ollama: ollama:
base-url: http://127.0.0.1:11434 base-url: http://127.0.0.1:11434
chat: chat:
@ -207,7 +208,7 @@ spring:
stabilityai: stabilityai:
api-key: sk-e53UqbboF8QJCscYvzJscJxJXoFcFg4iJjl1oqgE7baJETmx api-key: sk-e53UqbboF8QJCscYvzJscJxJXoFcFg4iJjl1oqgE7baJETmx
dashscope: # 通义千问 dashscope: # 通义千问
api-key: sk-71800982914041848008480000000000 api-key: sk-47aa124781be4bfb95244cc62f6xxxx
minimax: # Minimaxhttps://www.minimaxi.com/ minimax: # Minimaxhttps://www.minimaxi.com/
api-key: xxxx api-key: xxxx
moonshot: # 月之暗灭KIMI moonshot: # 月之暗灭KIMI
@ -217,9 +218,30 @@ spring:
chat: chat:
options: options:
model: deepseek-chat model: deepseek-chat
model:
rerank: false # 是否开启“通义千问”的 Rerank 模型,填写 dashscope 开启
mcp:
server:
enabled: true
name: yudao-mcp-server
version: 1.0.0
instructions: 一个 MCP 示例服务
sse-endpoint: /sse
client:
enabled: true
name: mcp
sse:
connections:
filesystem:
url: http://127.0.0.1:8089
sse-endpoint: /sse
yudao: yudao:
ai: ai:
gemini: # 谷歌 Gemini
enable: true
api-key: AIzaSyAVoBxgoFvvte820vEQMma2LKBnC98bqMQ
model: gemini-2.5-flash
doubao: # 字节豆包 doubao: # 字节豆包
enable: true enable: true
api-key: 5c1b5747-26d2-4ebd-a4e0-dd0e8d8b4272 api-key: 5c1b5747-26d2-4ebd-a4e0-dd0e8d8b4272
@ -236,7 +258,7 @@ yudao:
enable: true enable: true
appKey: 75b161ed2aef4719b275d6e7f2a4d4cd appKey: 75b161ed2aef4719b275d6e7f2a4d4cd
secretKey: YWYxYWI2MTA4ODI2NGZlYTQyNjAzZTcz secretKey: YWYxYWI2MTA4ODI2NGZlYTQyNjAzZTcz
model: generalv3.5 model: x1
baichuan: # 百川智能 baichuan: # 百川智能
enable: true enable: true
api-key: sk-abc api-key: sk-abc
@ -251,6 +273,9 @@ yudao:
enable: true enable: true
# base-url: https://suno-55ishh05u-status2xxs-projects.vercel.app # base-url: https://suno-55ishh05u-status2xxs-projects.vercel.app
base-url: http://127.0.0.1:3001 base-url: http://127.0.0.1:3001
web-search:
enable: true
api-key: sk-40500e52840f4d24b956d0b1d80d9abe
--- #################### 芋道相关配置 #################### --- #################### 芋道相关配置 ####################