diff --git a/.gitignore b/.gitignore index a71cd2b..0b3809c 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,4 @@ build/ ### VS Code ### .vscode/ +/logs/ diff --git a/pom.xml b/pom.xml index 5fed99c..51f2eb1 100644 --- a/pom.xml +++ b/pom.xml @@ -17,39 +17,27 @@ 1.0.3 3.19.0 1.18.40 + 4.38.0 + 3.9.1 + 33.0.0-jre org.springframework.boot spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-logging + + - com.github.ulisesbocchio jasypt-spring-boot-starter 3.0.5 - - - - org.springframework.ai - spring-ai-starter-model-deepseek - - - - org.springframework.ai - spring-ai-starter-model-ollama - - - - org.springframework.ai - spring-ai-starter-model-zhipuai - - - - org.springframework.ai - spring-ai-starter-model-openai - com.alibaba.cloud.ai @@ -65,19 +53,6 @@ spring-boot-starter-test test - - - com.alibaba - dashscope-sdk-java - 2.21.13 - - - org.slf4j - slf4j-simple - - - - org.apache.commons commons-lang3 @@ -88,6 +63,53 @@ lombok ${lombok.version} + + com.github.victools + jsonschema-generator + ${jsonschema-generator.version} + + + org.postgresql + postgresql + runtime + + + org.springframework.ai + spring-ai-starter-vector-store-pgvector + + + + com.baomidou + mybatis-plus-spring-boot3-starter + + + + p6spy + p6spy + ${p6spy.version} + + + + org.springframework.boot + spring-boot-starter-aop + + + + org.springframework.boot + spring-boot-starter-log4j2 + + + + org.springframework.boot + spring-boot-starter-validation + + + + com.google.guava + guava + ${guava.version} + + @@ -105,6 +127,14 @@ pom import + + + com.baomidou + mybatis-plus-bom + 3.5.14 + pom + import + diff --git a/src/main/java/com/hanserwei/airobot/advisor/CustomStreamLoggerAdvisor.java b/src/main/java/com/hanserwei/airobot/advisor/CustomStreamLoggerAdvisor.java new file mode 100644 index 0000000..f2777f4 --- /dev/null +++ b/src/main/java/com/hanserwei/airobot/advisor/CustomStreamLoggerAdvisor.java @@ -0,0 +1,63 @@ +package com.hanserwei.airobot.advisor; + +import lombok.extern.slf4j.Slf4j; +import org.jetbrains.annotations.NotNull; +import org.springframework.ai.chat.client.ChatClientRequest; +import org.springframework.ai.chat.client.ChatClientResponse; +import org.springframework.ai.chat.client.advisor.api.StreamAdvisor; +import org.springframework.ai.chat.client.advisor.api.StreamAdvisorChain; +import reactor.core.publisher.Flux; + +import java.util.concurrent.atomic.AtomicReference; + +@Slf4j +public class CustomStreamLoggerAdvisor implements StreamAdvisor { + + @Override + public int getOrder() { + return 99; // order 值越小,越先执行 + } + + @NotNull + @Override + public String getName() { + return this.getClass().getSimpleName(); + } + + @NotNull + @Override + public Flux adviseStream(@NotNull ChatClientRequest chatClientRequest, StreamAdvisorChain streamAdvisorChain) { + + Flux chatClientResponseFlux = streamAdvisorChain.nextStream(chatClientRequest); + + // 创建 AI 流式回答聚合容器(线程安全) + AtomicReference fullContent = new AtomicReference<>(new StringBuilder()); + + // 返回处理后的流 + return chatClientResponseFlux + .doOnNext(response -> { + // 逐块收集内容 + String chunk = null; + if (response.chatResponse() != null) { + chunk = response.chatResponse().getResult().getOutput().getText(); + } + + log.info("## chunk: {}", chunk); + + // 若 chunk 块不为空,则追加到 fullContent 中 + if (chunk != null) { + fullContent.get().append(chunk); + } + }) + .doOnComplete(() -> { + // 流完成后打印完整回答 + String completeResponse = fullContent.get().toString(); + log.info("\n==== FULL AI RESPONSE ====\n{}\n========================", completeResponse); + }) + .doOnError(error -> { + // 出错时打印已收集的部分 + String partialResponse = fullContent.get().toString(); + log.error("## Stream 流出现错误,已收集回答如下: {}", partialResponse, error); + }); + } +} diff --git a/src/main/java/com/hanserwei/airobot/advisor/MyLoggerAdvisor.java b/src/main/java/com/hanserwei/airobot/advisor/MyLoggerAdvisor.java deleted file mode 100644 index dd85e1d..0000000 --- a/src/main/java/com/hanserwei/airobot/advisor/MyLoggerAdvisor.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.hanserwei.airobot.advisor; - -import lombok.extern.slf4j.Slf4j; -import org.springframework.ai.chat.client.ChatClientRequest; -import org.springframework.ai.chat.client.ChatClientResponse; -import org.springframework.ai.chat.client.advisor.api.CallAdvisor; -import org.springframework.ai.chat.client.advisor.api.CallAdvisorChain; - -@Slf4j -public class MyLoggerAdvisor implements CallAdvisor { - - @Override - public ChatClientResponse adviseCall(ChatClientRequest chatClientRequest, CallAdvisorChain callAdvisorChain) { - log.info("## 请求入参: {}", chatClientRequest); - ChatClientResponse chatClientResponse = callAdvisorChain.nextCall(chatClientRequest); - log.info("## 请求出参: {}", chatClientResponse); - return chatClientResponse; - } - - @Override - public int getOrder() { - return 1; // order 值越小,越先执行 - } - - @Override - public String getName() { - // 获取类名称 - return this.getClass().getSimpleName(); - } -} \ No newline at end of file diff --git a/src/main/java/com/hanserwei/airobot/aspect/ApiOperationLog.java b/src/main/java/com/hanserwei/airobot/aspect/ApiOperationLog.java new file mode 100644 index 0000000..25fe119 --- /dev/null +++ b/src/main/java/com/hanserwei/airobot/aspect/ApiOperationLog.java @@ -0,0 +1,16 @@ +package com.hanserwei.airobot.aspect; + +import java.lang.annotation.*; + +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD}) +@Documented +public @interface ApiOperationLog { + /** + * API 功能描述 + * + * @return API 功能描述 + */ + String description() default ""; + +} \ No newline at end of file diff --git a/src/main/java/com/hanserwei/airobot/aspect/ApiOperationLogAspect.java b/src/main/java/com/hanserwei/airobot/aspect/ApiOperationLogAspect.java new file mode 100644 index 0000000..8c381f3 --- /dev/null +++ b/src/main/java/com/hanserwei/airobot/aspect/ApiOperationLogAspect.java @@ -0,0 +1,101 @@ +package com.hanserwei.airobot.aspect; + +import com.hanserwei.airobot.utils.JsonUtil; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.aspectj.lang.reflect.MethodSignature; +import org.springframework.stereotype.Component; + +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * API操作日志切面类,用于记录被 @ApiOperationLog 注解标记的方法的执行信息, + * 包括方法描述、入参、出参以及执行耗时等。 + */ +@Aspect +@Component +@Slf4j +public class ApiOperationLogAspect { + + /** 以自定义 @ApiOperationLog 注解为切点,凡是添加 @ApiOperationLog 的方法,都会执行环绕中的代码 */ + @Pointcut("@annotation(com.hanserwei.airobot.aspect.ApiOperationLog)") + public void apiOperationLog() {} + + /** + * 环绕通知方法,用于记录目标方法的执行日志。 + * 包括方法开始时间、类名、方法名、入参、功能描述、执行结果和耗时。 + * + * @param joinPoint 切点对象,封装了目标方法的相关信息 + * @return 目标方法的返回值 + * @throws Throwable 目标方法可能抛出的异常 + */ + @Around("apiOperationLog()") + public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable { + // 请求开始时间 + long startTime = System.currentTimeMillis(); + + // 获取被请求的类和方法 + String className = joinPoint.getTarget().getClass().getSimpleName(); + String methodName = joinPoint.getSignature().getName(); + + // 请求入参 + Object[] args = joinPoint.getArgs(); + // 入参转 JSON 字符串 + String argsJsonStr = Arrays.stream(args).map(toJsonStr()).collect(Collectors.joining(", ")); + + // 功能描述信息 + String description = getApiOperationLogDescription(joinPoint); + + // 打印请求相关参数 + log.info("====== 请求开始: [{}], 入参: {}, 请求类: {}, 请求方法: {} =================================== ", + description, argsJsonStr, className, methodName); + + // 执行切点方法 + Object result = joinPoint.proceed(); + + // 执行耗时 + long executionTime = System.currentTimeMillis() - startTime; + + // 打印出参等相关信息 + log.info("====== 请求结束: [{}], 耗时: {}ms, 出参: {} =================================== ", + description, executionTime, JsonUtil.toJsonString(result)); + + return result; + } + + /** + * 获取目标方法上 @ApiOperationLog 注解的描述信息。 + * + * @param joinPoint 切点对象,用于获取目标方法信息 + * @return 注解中定义的功能描述字符串 + */ + private String getApiOperationLogDescription(ProceedingJoinPoint joinPoint) { + // 1. 从 ProceedingJoinPoint 获取 MethodSignature + MethodSignature signature = (MethodSignature) joinPoint.getSignature(); + + // 2. 使用 MethodSignature 获取当前被注解的 Method + Method method = signature.getMethod(); + + // 3. 从 Method 中提取 LogExecution 注解 + ApiOperationLog apiOperationLog = method.getAnnotation(ApiOperationLog.class); + + // 4. 从 LogExecution 注解中获取 description 属性 + return apiOperationLog.description(); + } + + /** + * 返回一个将对象转换为 JSON 字符串的函数。 + * + * @return 将对象序列化为 JSON 字符串的函数 + */ + private Function toJsonStr() { + return JsonUtil::toJsonString; + } + +} diff --git a/src/main/java/com/hanserwei/airobot/config/ChatClientConfig.java b/src/main/java/com/hanserwei/airobot/config/ChatClientConfig.java index a3980f9..c0811b7 100644 --- a/src/main/java/com/hanserwei/airobot/config/ChatClientConfig.java +++ b/src/main/java/com/hanserwei/airobot/config/ChatClientConfig.java @@ -1,20 +1,16 @@ package com.hanserwei.airobot.config; import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel; -import com.hanserwei.airobot.advisor.MyLoggerAdvisor; import jakarta.annotation.Resource; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor; import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor; import org.springframework.ai.chat.memory.ChatMemory; -import org.springframework.ai.deepseek.DeepSeekChatModel; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class ChatClientConfig { - @Resource - private ChatMemory chatMemory; /** * 初始化 ChatClient 客户端 @@ -25,11 +21,6 @@ public class ChatClientConfig { @Bean public ChatClient chatClient(DashScopeChatModel chatModel) { return ChatClient.builder(chatModel) -// .defaultSystem("请你扮演一名犬小哈 Java 项目实战专栏的客服人员") - .defaultAdvisors( - new SimpleLoggerAdvisor(), - new MyLoggerAdvisor(), - MessageChatMemoryAdvisor.builder(chatMemory).build()) .build(); } } \ No newline at end of file diff --git a/src/main/java/com/hanserwei/airobot/config/ChatMemoryConfig.java b/src/main/java/com/hanserwei/airobot/config/ChatMemoryConfig.java deleted file mode 100644 index 48158ff..0000000 --- a/src/main/java/com/hanserwei/airobot/config/ChatMemoryConfig.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.hanserwei.airobot.config; - -import jakarta.annotation.Resource; -import org.springframework.ai.chat.memory.ChatMemory; -import org.springframework.ai.chat.memory.ChatMemoryRepository; -import org.springframework.ai.chat.memory.MessageWindowChatMemory; -import org.springframework.ai.chat.memory.repository.cassandra.CassandraChatMemoryRepository; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -@Configuration -public class ChatMemoryConfig { - - /** - * 记忆存储 - */ - @Resource - private CassandraChatMemoryRepository chatMemoryRepository; - - /** - * 初始化一个 ChatMemory 实例,并注入到 Spring 容器中 - * @return ChatMemory - */ - @Bean - public ChatMemory chatMemory() { - return MessageWindowChatMemory.builder() - .maxMessages(50) // 最大消息窗口为 50,默认值为 20 - .chatMemoryRepository(chatMemoryRepository) // 记忆存储 - .build(); - } -} \ No newline at end of file diff --git a/src/main/java/com/hanserwei/airobot/config/JacksonConfig.java b/src/main/java/com/hanserwei/airobot/config/JacksonConfig.java new file mode 100644 index 0000000..cbb0207 --- /dev/null +++ b/src/main/java/com/hanserwei/airobot/config/JacksonConfig.java @@ -0,0 +1,65 @@ +package com.hanserwei.airobot.config; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer; +import com.fasterxml.jackson.datatype.jsr310.deser.YearMonthDeserializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer; +import com.fasterxml.jackson.datatype.jsr310.ser.YearMonthSerializer; +import com.hanserwei.airobot.constant.DateConstants; +import com.hanserwei.airobot.utils.JsonUtil; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.YearMonth; +import java.util.TimeZone; + +@Configuration +public class JacksonConfig { + + @Bean + public ObjectMapper objectMapper() { + // 初始化一个 ObjectMapper 对象,用于自定义 Jackson 的行为 + ObjectMapper objectMapper = new ObjectMapper(); + + // 忽略未知属性 + objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false); + // 设置凡是为 null 的字段,返参中均不返回,请根据项目组约定是否开启 + // objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + + // 设置时区 + objectMapper.setTimeZone(TimeZone.getTimeZone("Asia/Shanghai")); + + // JavaTimeModule 用于指定序列化和反序列化规则 + JavaTimeModule javaTimeModule = new JavaTimeModule(); + + // 支持 LocalDateTime、LocalDate、LocalTime + javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateConstants.DATE_FORMAT_Y_M_D_H_M_S)); + javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateConstants.DATE_FORMAT_Y_M_D_H_M_S)); + javaTimeModule.addSerializer(LocalDate.class, new LocalDateSerializer(DateConstants.DATE_FORMAT_Y_M_D)); + javaTimeModule.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateConstants.DATE_FORMAT_Y_M_D)); + javaTimeModule.addSerializer(LocalTime.class, new LocalTimeSerializer(DateConstants.DATE_FORMAT_H_M_S)); + javaTimeModule.addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateConstants.DATE_FORMAT_H_M_S)); + // 支持 YearMonth + javaTimeModule.addSerializer(YearMonth.class, new YearMonthSerializer(DateConstants.DATE_FORMAT_Y_M)); + javaTimeModule.addDeserializer(YearMonth.class, new YearMonthDeserializer(DateConstants.DATE_FORMAT_Y_M)); + + objectMapper.registerModule(javaTimeModule); + + // 初始化 JsonUtils 中的 ObjectMapper + JsonUtil.init(objectMapper); + + return objectMapper; + } + +} \ No newline at end of file diff --git a/src/main/java/com/hanserwei/airobot/config/MybatisPlusConfig.java b/src/main/java/com/hanserwei/airobot/config/MybatisPlusConfig.java new file mode 100644 index 0000000..9db43da --- /dev/null +++ b/src/main/java/com/hanserwei/airobot/config/MybatisPlusConfig.java @@ -0,0 +1,9 @@ +package com.hanserwei.airobot.config; + +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.context.annotation.Configuration; + +@Configuration +@MapperScan("com.hanserwei.airobot.domain.mapper") +public class MybatisPlusConfig { +} \ No newline at end of file diff --git a/src/main/java/com/hanserwei/airobot/constant/DateConstants.java b/src/main/java/com/hanserwei/airobot/constant/DateConstants.java new file mode 100644 index 0000000..dd0938e --- /dev/null +++ b/src/main/java/com/hanserwei/airobot/constant/DateConstants.java @@ -0,0 +1,36 @@ +package com.hanserwei.airobot.constant; + +import java.time.format.DateTimeFormatter; + +public interface DateConstants { + + /** + * DateTimeFormatter:年-月-日 时:分:秒 + */ + DateTimeFormatter DATE_FORMAT_Y_M_D_H_M_S = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + /** + * DateTimeFormatter:年-月-日 + */ + DateTimeFormatter DATE_FORMAT_Y_M_D = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + + /** + * DateTimeFormatter:月-日 + */ + DateTimeFormatter DATE_FORMAT_M_D = DateTimeFormatter.ofPattern("MM-dd"); + + /** + * DateTimeFormatter:时:分:秒 + */ + DateTimeFormatter DATE_FORMAT_H_M_S = DateTimeFormatter.ofPattern("HH:mm:ss"); + + /** + * DateTimeFormatter:时:分 + */ + DateTimeFormatter DATE_FORMAT_H_M = DateTimeFormatter.ofPattern("HH:mm"); + + /** + * DateTimeFormatter:年-月 + */ + DateTimeFormatter DATE_FORMAT_Y_M = DateTimeFormatter.ofPattern("yyyy-MM"); +} \ No newline at end of file diff --git a/src/main/java/com/hanserwei/airobot/controller/ChatClientController.java b/src/main/java/com/hanserwei/airobot/controller/ChatClientController.java deleted file mode 100644 index f240484..0000000 --- a/src/main/java/com/hanserwei/airobot/controller/ChatClientController.java +++ /dev/null @@ -1,49 +0,0 @@ -package com.hanserwei.airobot.controller; - -import jakarta.annotation.Resource; -import org.springframework.ai.chat.client.ChatClient; -import org.springframework.ai.chat.memory.ChatMemory; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; -import reactor.core.publisher.Flux; - -@RestController -@RequestMapping("/v2/ai") -public class ChatClientController { - - @Resource - private ChatClient chatClient; - - /** - * 普通对话 - * - * @param message 对话输入内容 - * @return 对话结果 - */ - @GetMapping("/generate") - public String generate(@RequestParam(value = "message", defaultValue = "你是谁?") String message) { - // 一次性返回结果 - return chatClient.prompt() - .user(message) - .call() - .content(); - } - - /** - * 流式对话 - * @param message 对话输入内容 - * @return 对话结果 - */ - @GetMapping(value = "/generateStream", produces = "text/html;charset=utf-8") - public Flux generateStream(@RequestParam(value = "message", defaultValue = "你是谁?") String message, - @RequestParam(value = "chatId") String chatId) { - return chatClient.prompt() - .user(message) // 提示词 - .advisors(a -> a.param(ChatMemory.CONVERSATION_ID, chatId)) - .stream() // 流式输出 - .content(); - - } -} \ No newline at end of file diff --git a/src/main/java/com/hanserwei/airobot/controller/ChatController.java b/src/main/java/com/hanserwei/airobot/controller/ChatController.java new file mode 100644 index 0000000..178e6b9 --- /dev/null +++ b/src/main/java/com/hanserwei/airobot/controller/ChatController.java @@ -0,0 +1,87 @@ +package com.hanserwei.airobot.controller; + +import com.alibaba.cloud.ai.dashscope.api.DashScopeApi; +import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel; +import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatOptions; +import com.google.common.collect.Lists; +import com.hanserwei.airobot.advisor.CustomStreamLoggerAdvisor; +import com.hanserwei.airobot.aspect.ApiOperationLog; +import com.hanserwei.airobot.model.vo.chat.AiChatReqVO; +import com.hanserwei.airobot.model.vo.chat.AiResponse; +import com.hanserwei.airobot.model.vo.chat.NewChatReqVO; +import com.hanserwei.airobot.service.ChatService; +import com.hanserwei.airobot.utils.Response; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.ai.chat.client.ChatClient; +import org.springframework.ai.chat.client.advisor.api.Advisor; +import org.springframework.ai.chat.model.ChatModel; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.MediaType; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import reactor.core.publisher.Flux; + +import java.util.List; + +@RestController +@RequestMapping("/chat") +@Slf4j +public class ChatController { + + @Resource + private ChatService chatService; + + @Value("${spring.ai.dashscope.api-key}") + private String apiKey; + + @PostMapping("/new") + @ApiOperationLog(description = "新建对话") + public Response newChat(@RequestBody @Validated NewChatReqVO newChatReqVO) { + return chatService.newChat(newChatReqVO); + } + + @PostMapping(value = "/completion", produces = MediaType.TEXT_EVENT_STREAM_VALUE) + @ApiOperationLog(description = "流式对话") + public Flux chat(@RequestBody @Validated AiChatReqVO aiChatReqVO) { + // 用户消息 + String message = aiChatReqVO.getMessage(); + // 模型名称 + String modelName = aiChatReqVO.getModelName(); + // 温度 + Double temperature = aiChatReqVO.getTemperature(); + + // 构建ChatModel + ChatModel chatModel = DashScopeChatModel.builder() + .dashScopeApi(DashScopeApi.builder() + .apiKey(apiKey) + .build()) + .build(); + + // 动态设置模型名称和温度 + ChatClient.ChatClientRequestSpec chatClientRequestSpec = ChatClient.create(chatModel) + .prompt() + .options(DashScopeChatOptions.builder() + .withModel(modelName) + .withTemperature(temperature) + .build()) + .user(message); + + // Advisor 集合 + List advisors = Lists.newArrayList(); + // 添加自定义打印流式对话日志 Advisor + advisors.add(new CustomStreamLoggerAdvisor()); + + // 应用 Advisor 集合 + chatClientRequestSpec.advisors(advisors); + + // 流式输出 + return chatClientRequestSpec.stream() + .content() + .mapNotNull(text -> AiResponse.builder().v(text).build()); + } + +} \ No newline at end of file diff --git a/src/main/java/com/hanserwei/airobot/controller/DashscopeAIController.java b/src/main/java/com/hanserwei/airobot/controller/DashscopeAIController.java deleted file mode 100644 index c1d7225..0000000 --- a/src/main/java/com/hanserwei/airobot/controller/DashscopeAIController.java +++ /dev/null @@ -1,103 +0,0 @@ -package com.hanserwei.airobot.controller; - -import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel; -import com.hanserwei.airobot.model.AIResponse; -import jakarta.annotation.Resource; -import org.springframework.ai.chat.client.ChatClient; -import org.springframework.ai.chat.messages.AssistantMessage; -import org.springframework.ai.chat.messages.UserMessage; -import org.springframework.ai.chat.model.Generation; -import org.springframework.ai.chat.prompt.Prompt; -import org.springframework.ai.chat.prompt.PromptTemplate; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.http.MediaType; -import org.springframework.ai.chat.messages.Message; -import org.springframework.util.CollectionUtils; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; -import reactor.core.publisher.Flux; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.concurrent.ConcurrentHashMap; - -@RestController -@RequestMapping("/v6/ai") -public class DashscopeAIController { - - // 存储聊天对话 - private final Map> chatMemoryStore = new ConcurrentHashMap<>(); - - @Resource - private DashScopeChatModel dashScopeChatModel; - - @Value("classpath:/prompts/code-assistant.st") - private org.springframework.core.io.Resource templateResource; - - /** - * 普通对话 - * - * @param message 对话输入内容 - * @return 对话结果 - */ - @GetMapping("/generate") - public String generate(@RequestParam(value = "message", defaultValue = "你是谁?") String message, - @RequestParam(value = "chatId") String chatId) - { - // 提示词模板 - PromptTemplate promptTemplate = new PromptTemplate(templateResource); - // 根据 chatId 获取对话记录 - List messages = chatMemoryStore.get(chatId); - // 若不存在,则初始化一份 - if (CollectionUtils.isEmpty(messages)) { - messages = new ArrayList<>(); - chatMemoryStore.put(chatId, messages); - } - - // 添加 “用户角色消息” 到聊天记录中 - messages.add(new UserMessage(message)); - - // 构建提示词 - Prompt prompt = new Prompt(messages); - // 一次性返回结果 - ChatClient chatClient = ChatClient.builder(dashScopeChatModel).build(); - String responseText = Objects.requireNonNull(chatClient.prompt(prompt) - .call() - .chatResponse()) - .getResult() - .getOutput() - .getText(); - // 添加 “助手角色消息” 到聊天记录中 - if (responseText != null) { - messages.add(new AssistantMessage(responseText)); - } - - return responseText; - } - - /** - * 流式对话 - * - * @param message 对话输入内容 - * @return 对话结果 - */ - @GetMapping(value = "/generateStream", produces = MediaType.TEXT_EVENT_STREAM_VALUE) - public Flux generateStream(@RequestParam(value = "message", defaultValue = "你是谁?") String message) { - ChatClient chatClient = ChatClient.builder(dashScopeChatModel).build(); - return chatClient.prompt() - .user(message) - .stream() - .chatResponse() - .mapNotNull( - chatResponse -> { - Generation generation = chatResponse.getResult(); - String text = generation.getOutput().getText(); - return AIResponse.builder().v(text).build(); - } - ); - } -} diff --git a/src/main/java/com/hanserwei/airobot/controller/DeepSeekChatController.java b/src/main/java/com/hanserwei/airobot/controller/DeepSeekChatController.java deleted file mode 100644 index 5e1ab0e..0000000 --- a/src/main/java/com/hanserwei/airobot/controller/DeepSeekChatController.java +++ /dev/null @@ -1,46 +0,0 @@ -package com.hanserwei.airobot.controller; - -import jakarta.annotation.Resource; -import org.springframework.ai.chat.messages.UserMessage; -import org.springframework.ai.chat.prompt.Prompt; -import org.springframework.ai.deepseek.DeepSeekChatModel; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; -import reactor.core.publisher.Flux; - -@RestController -@RequestMapping("/ai") -public class DeepSeekChatController { - - @Resource - private DeepSeekChatModel chatModel; - - /** - * 普通对话 - * @param message 对话输入内容 - * @return 对话结果 - */ - @GetMapping("/generate") - public String generate(@RequestParam(value = "message", defaultValue = "你是谁?") String message) { - // 一次性返回结果 - return chatModel.call(message); - } - - /** - * 流式对话 - * @param message 对话输入内容 - * @return 对话结果 - */ - @GetMapping(value = "/generateStream", produces = "text/html;charset=utf-8") - public Flux generateStream(@RequestParam(value = "message", defaultValue = "你是谁?") String message) { - // 构建提示词 - Prompt prompt = new Prompt(new UserMessage(message)); - - // 流式输出 - return chatModel.stream(prompt) - .mapNotNull(chatResponse -> chatResponse.getResult().getOutput().getText()); - } - -} \ No newline at end of file diff --git a/src/main/java/com/hanserwei/airobot/controller/DeepSeekR1ChatController.java b/src/main/java/com/hanserwei/airobot/controller/DeepSeekR1ChatController.java deleted file mode 100644 index 8fdce1e..0000000 --- a/src/main/java/com/hanserwei/airobot/controller/DeepSeekR1ChatController.java +++ /dev/null @@ -1,71 +0,0 @@ -package com.hanserwei.airobot.controller; - -import jakarta.annotation.Resource; -import org.apache.commons.lang3.StringUtils; -import org.springframework.ai.chat.messages.UserMessage; -import org.springframework.ai.chat.prompt.Prompt; -import org.springframework.ai.deepseek.DeepSeekAssistantMessage; -import org.springframework.ai.deepseek.DeepSeekChatModel; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; -import reactor.core.publisher.Flux; - -import java.util.Objects; -import java.util.concurrent.atomic.AtomicBoolean; - -@RestController -@RequestMapping("/v1/ai") -public class DeepSeekR1ChatController { - - @Resource - private DeepSeekChatModel chatModel; - - /** - * 流式对话 - * @param message 对话输入内容 - * @return 对话结果 - */ - @GetMapping(value = "/generateStream", produces = "text/html;charset=utf-8") - public Flux generateStream(@RequestParam(value = "message", defaultValue = "你是谁?") String message) { - // 构建提示词 - Prompt prompt = new Prompt(new UserMessage(message)); - - // 使用原子布尔值跟踪分隔线状态(每个请求独立) - AtomicBoolean needSeparator = new AtomicBoolean(true); - - // 流式输出 - return chatModel.stream(prompt) - .mapNotNull(chatResponse -> { - // 获取响应内容 - DeepSeekAssistantMessage deepSeekAssistantMessage = (DeepSeekAssistantMessage) chatResponse.getResult().getOutput(); - // 推理内容 - String reasoningContent = deepSeekAssistantMessage.getReasoningContent(); - // 推理结束后的正式回答 - String text = deepSeekAssistantMessage.getText(); - - // 是否是正式回答 - boolean isTextResponse = false; - // 若推理内容有值,则响应推理内容,否则,说明推理结束了,响应正式回答 - String rawContent; - if (Objects.isNull(text)) { - rawContent = reasoningContent; - } else { - rawContent = text; - isTextResponse = true; // 标记为正式回答 - } - - // 处理换行 - String processed = StringUtils.isNotBlank(rawContent) ? rawContent.replace("\n", "
") : rawContent; - - // 在正式回答内容之前,添加一个分隔线 - if (isTextResponse - && needSeparator.compareAndSet(true, false)) { - processed = "
" + processed; // 使用 HTML 的
标签实现 - } - - return processed; - }); - } -} \ No newline at end of file diff --git a/src/main/java/com/hanserwei/airobot/controller/MultimodalityController.java b/src/main/java/com/hanserwei/airobot/controller/MultimodalityController.java deleted file mode 100644 index 926ee05..0000000 --- a/src/main/java/com/hanserwei/airobot/controller/MultimodalityController.java +++ /dev/null @@ -1,64 +0,0 @@ -package com.hanserwei.airobot.controller; - -import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel; -import jakarta.annotation.Resource; -import org.springframework.ai.chat.messages.UserMessage; -import org.springframework.ai.chat.model.Generation; -import org.springframework.ai.chat.prompt.Prompt; -import org.springframework.ai.content.Media; -import org.springframework.ai.openai.OpenAiChatModel; -import org.springframework.core.io.ClassPathResource; -import org.springframework.util.MimeTypeUtils; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; -import reactor.core.publisher.Flux; - -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -@RestController -@RequestMapping("/v9/ai") -public class MultimodalityController { - - @Resource - private DashScopeChatModel chatModel; - - /** - * 流式对话 - * @param message - * @return - */ - @GetMapping(value = "/generateStream", produces = "text/html;charset=utf-8") - public Flux generateStream(@RequestParam(value = "message") String message) { - // 1. 创建媒体资源 - Media image = new Media( - MimeTypeUtils.IMAGE_PNG, - new ClassPathResource("/images/img.png") - ); - - // 2. 附加选项(可选),如温度值等等 - Map metadata = new HashMap<>(); - metadata.put("temperature", 0.7); - - // 3. 构建多模态消息 - UserMessage userMessage = UserMessage.builder() - .text(message) - .media(image) - .metadata(metadata) - .build(); - - // 4. 构建提示词 - Prompt prompt = new Prompt(List.of(userMessage)); - // 5. 流式调用 - return chatModel.stream(prompt) - .mapNotNull(chatResponse -> { - Generation generation = chatResponse.getResult(); - return generation.getOutput().getText(); - }); - - } - -} \ No newline at end of file diff --git a/src/main/java/com/hanserwei/airobot/controller/OllamaController.java b/src/main/java/com/hanserwei/airobot/controller/OllamaController.java deleted file mode 100644 index 0334e66..0000000 --- a/src/main/java/com/hanserwei/airobot/controller/OllamaController.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.hanserwei.airobot.controller; - -import jakarta.annotation.Resource; -import org.springframework.ai.chat.model.ChatResponse; -import org.springframework.ai.chat.prompt.Prompt; -import org.springframework.ai.ollama.OllamaChatModel; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; - -@RestController -@RequestMapping("/v3/ai") -public class OllamaController { - - @Resource - private OllamaChatModel chatModel; - - /** - * 普通对话 - * @param message 对话输入内容 - * @return 对话结果 - */ - @GetMapping("/generate") - public String generate(@RequestParam(value = "message", defaultValue = "你是谁?") String message) { - // 构建提示词,调用大模型 - ChatResponse chatResponse = chatModel.call(new Prompt(message)); - - // 响应回答内容 - return chatResponse.getResult().getOutput().getText(); - } - -} \ No newline at end of file diff --git a/src/main/java/com/hanserwei/airobot/controller/OpenAIController.java b/src/main/java/com/hanserwei/airobot/controller/OpenAIController.java deleted file mode 100644 index cbe1ed6..0000000 --- a/src/main/java/com/hanserwei/airobot/controller/OpenAIController.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.hanserwei.airobot.controller; - -import jakarta.annotation.Resource; -import org.springframework.ai.chat.messages.UserMessage; -import org.springframework.ai.chat.model.Generation; -import org.springframework.ai.chat.prompt.Prompt; -import org.springframework.ai.openai.OpenAiChatModel; -import org.springframework.http.MediaType; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; -import reactor.core.publisher.Flux; - -@RestController -@RequestMapping("/v5/ai") -public class OpenAIController { - - @Resource - private OpenAiChatModel chatModel; - - /** - * 普通对话 - * @param message 对话输入内容 - * @return 对话结果 - */ - @GetMapping("/generate") - public String generate(@RequestParam(value = "message", defaultValue = "你是谁?") String message) { - // 一次性返回结果 - return chatModel.call(message); - } - - /** - * 流式对话 - * @param message 对话输入内容 - * @return 对话结果 - */ - @GetMapping(value = "/generateStream", produces = MediaType.TEXT_EVENT_STREAM_VALUE) - public Flux generateStream(@RequestParam(value = "message", defaultValue = "你是谁?") String message) { - // 构建提示词 - Prompt prompt = new Prompt(new UserMessage(message)); - - // 流式输出 - return chatModel.stream(prompt) - .mapNotNull(chatResponse -> { - Generation generation = chatResponse.getResult(); - return generation.getOutput().getText(); - }); - - } - -} \ No newline at end of file diff --git a/src/main/java/com/hanserwei/airobot/controller/PromptTemplateController.java b/src/main/java/com/hanserwei/airobot/controller/PromptTemplateController.java deleted file mode 100644 index 4e5b955..0000000 --- a/src/main/java/com/hanserwei/airobot/controller/PromptTemplateController.java +++ /dev/null @@ -1,135 +0,0 @@ -package com.hanserwei.airobot.controller; - -import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel; -import com.hanserwei.airobot.model.AIResponse; -import jakarta.annotation.Resource; -import org.springframework.ai.chat.messages.Message; -import org.springframework.ai.chat.model.Generation; -import org.springframework.ai.chat.prompt.Prompt; -import org.springframework.ai.chat.prompt.PromptTemplate; -import org.springframework.ai.chat.prompt.SystemPromptTemplate; -import org.springframework.ai.template.st.StTemplateRenderer; -import org.springframework.http.MediaType; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; -import reactor.core.publisher.Flux; - -import java.util.List; -import java.util.Map; - -@RestController -@RequestMapping("/v7/ai") -public class PromptTemplateController { - - @Resource - private DashScopeChatModel chatModel; - - /** - * 智能代码生成 - * @param message - * @param lang - * @return - */ - @GetMapping(value = "/generateStream", produces = MediaType.TEXT_EVENT_STREAM_VALUE) - public Flux generateStream(@RequestParam(value = "message") String message, - @RequestParam(value = "lang") String lang) { - // 提示词模板 - String template = """ - 你是一位资深 {lang} 开发工程师。请严格遵循以下要求编写代码: - 1. 功能描述:{description} - 2. 代码需包含详细注释 - 3. 使用业界最佳实践 - """; - - PromptTemplate promptTemplate = new PromptTemplate(template); - - // 填充提示词占位符,转换为 Prompt 提示词对象 - Prompt prompt = promptTemplate.create(Map.of("description", message, "lang", lang)); - - // 流式输出 - return chatModel.stream(prompt) - .mapNotNull(chatResponse -> { - Generation generation = chatResponse.getResult(); - String text = generation.getOutput().getText(); - return AIResponse.builder().v(text).build(); - }); - } - - - /** - * 智能代码生成 2 - * @param message - * @param lang - * @return - */ - @GetMapping(value = "/generateStream2", produces = MediaType.TEXT_EVENT_STREAM_VALUE) - public Flux generateStream2(@RequestParam(value = "message") String message, - @RequestParam(value = "lang") String lang) { - // 提示词模板 - PromptTemplate promptTemplate = PromptTemplate.builder() - .renderer(StTemplateRenderer.builder().startDelimiterToken('<').endDelimiterToken('>').build()) // 自定义占位符 - .template(""" - 你是一位资深 开发工程师。请严格遵循以下要求编写代码: - 1. 功能描述: - 2. 代码需包含详细注释 - 3. 使用业界最佳实践 - """) - .build(); - - // 填充提示词占位符,转换为 Prompt 提示词对象 - Prompt prompt = promptTemplate.create(Map.of("description", message, "lang", lang)); - - // 流式输出 - return chatModel.stream(prompt) - .mapNotNull(chatResponse -> { - Generation generation = chatResponse.getResult(); - String text = generation.getOutput().getText(); - return AIResponse.builder().v(text).build(); - }); - } - - /** - * 智能代码生成 3 - * @param message - * @param lang - * @return - */ - @GetMapping(value = "/generateStream3", produces = MediaType.TEXT_EVENT_STREAM_VALUE) - public Flux generateStream3(@RequestParam(value = "message") String message, - @RequestParam(value = "lang") String lang) { - - // 系统角色提示词模板 - String systemPrompt = """ - 你是一位资深 {lang} 开发工程师, 已经从业数十年,经验非常丰富。 - """; - SystemPromptTemplate systemPromptTemplate = new SystemPromptTemplate(systemPrompt); - // 填充提示词占位符,并转换为 Message 对象 - Message systemMessage = systemPromptTemplate.createMessage(Map.of("lang", lang)); - - // 用户角色提示词模板 - String userPrompt = """ - 请严格遵循以下要求编写代码: - 1. 功能描述:{description} - 2. 代码需包含详细注释 - 3. 使用业界最佳实践 - """; - PromptTemplate promptTemplate = new PromptTemplate(userPrompt); - // 填充提示词占位符,并转换为 Message 对象 - Message userMessage = promptTemplate.createMessage(Map.of("description", message)); - - - // 组合多角色消息,构建提示词 Prompt - Prompt prompt = new Prompt(List.of(systemMessage, userMessage)); - - // 流式输出 - return chatModel.stream(prompt) - .mapNotNull(chatResponse -> { - Generation generation = chatResponse.getResult(); - String text = generation.getOutput().getText(); - return AIResponse.builder().v(text).build(); - }); - } - -} \ No newline at end of file diff --git a/src/main/java/com/hanserwei/airobot/controller/StructuredOutputController.java b/src/main/java/com/hanserwei/airobot/controller/StructuredOutputController.java deleted file mode 100644 index 0bba37e..0000000 --- a/src/main/java/com/hanserwei/airobot/controller/StructuredOutputController.java +++ /dev/null @@ -1,127 +0,0 @@ -package com.hanserwei.airobot.controller; - -import com.hanserwei.airobot.model.ActorFilmography; -import com.hanserwei.airobot.model.Book; -import jakarta.annotation.Resource; -import org.springframework.ai.chat.client.ChatClient; -import org.springframework.ai.chat.prompt.Prompt; -import org.springframework.ai.chat.prompt.PromptTemplate; -import org.springframework.ai.converter.BeanOutputConverter; -import org.springframework.ai.converter.ListOutputConverter; -import org.springframework.ai.converter.MapOutputConverter; -import org.springframework.core.convert.support.DefaultConversionService; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; - -import java.util.List; -import java.util.Map; - -@RestController -@RequestMapping("/v8/ai") -public class StructuredOutputController { - - @Resource - private ChatClient chatClient; - - /** - * 示例1: BeanOutputConverter - 获取演员电影作品集 - * - * @param name - * @return - */ - @GetMapping("/actor/films") - public ActorFilmography generate(@RequestParam(value = "name") String name) { - // 一次性返回结果 - return chatClient.prompt() - .user(u -> u.text(""" - 请为演员 {actor} 生成包含5部代表作的电影作品集, - 只包含 {actor} 担任主演的电影,不要包含任何解释说明。 - """) - .param("actor", name)) - .call() - .entity(ActorFilmography.class); - } - - /** - * 示例2: MapOutputConverter - 获取编程语言信息 - * @param language - * @return - */ - @GetMapping("/language-info") - public Map getLanguageInfo(@RequestParam(value = "lang") String language) { - - String userText = """ - 请提供关于编程语言 {language} 的结构化信息,包含以下字段:" - name (语言名称), " - popularity (流行度排名,整数), " - features (主要特性,字符串数组), " - releaseYear (首次发布年份). " - 不要包含任何解释说明,直接输出 JSON 格式数据。 - """; - - return chatClient.prompt() - .user(u -> u.text(userText).param("language", language)) - .call() - .entity(new MapOutputConverter()); - } - - /** - * 示例3: ListOutputConverter - 获取城市列表 - * @param country - * @return - */ - @GetMapping("/city-list") - public List getCityList(@RequestParam(value = "country") String country) { - - return chatClient.prompt() - .user(u -> u.text( - """ - 列出 {country} 的8个主要城市名称。 - 不要包含任何编号、解释或其他文本,直接输出城市名称列表。 - """) - .param("country", country)) - .call() - .entity(new ListOutputConverter(new DefaultConversionService())); - } - - /** - * 使用低级 API 的 BeanOutputConverter - 获取书籍信息 - * @param bookTitle - * @return - */ - @GetMapping("/book-info") - public Book getBookInfo(@RequestParam(value = "name") String bookTitle) { - - // 使用 BeanOutputConverter 定义输出格式 - BeanOutputConverter converter = new BeanOutputConverter<>(Book.class); - - // 提示词模板 - String template = """ - 请提供关于书籍《{bookTitle}》的详细信息: - 1. 作者姓名 - 2. 出版年份 - 3. 主要类型(数组) - 4. 书籍描述(不少于50字) - - 不要包含任何解释说明,直接按指定格式输出。 - {format} - """; - - // 创建 Prompt - PromptTemplate promptTemplate = new PromptTemplate(template); - Prompt prompt = promptTemplate.create(Map.of( - "bookTitle", bookTitle, - "format", converter.getFormat() - )); - - // 调用模型并转换结果 - String result = chatClient.prompt(prompt) - .call() - .content(); - - // 结构化转换 - return converter.convert(result); - } -} \ No newline at end of file diff --git a/src/main/java/com/hanserwei/airobot/controller/Text2ImgController.java b/src/main/java/com/hanserwei/airobot/controller/Text2ImgController.java deleted file mode 100644 index e2f9306..0000000 --- a/src/main/java/com/hanserwei/airobot/controller/Text2ImgController.java +++ /dev/null @@ -1,54 +0,0 @@ -package com.hanserwei.airobot.controller; - -import com.alibaba.dashscope.aigc.imagesynthesis.ImageSynthesis; -import com.alibaba.dashscope.aigc.imagesynthesis.ImageSynthesisParam; -import com.alibaba.dashscope.aigc.imagesynthesis.ImageSynthesisResult; -import com.alibaba.dashscope.exception.ApiException; -import com.alibaba.dashscope.exception.NoApiKeyException; -import com.alibaba.dashscope.utils.JsonUtils; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; - -@RestController -@RequestMapping("/v10/ai") -@Slf4j -public class Text2ImgController { - - @Value("${spring.ai.dashscope.api-key}") - private String apiKey; - - /** - * 调用阿里百炼图生文大模型 - * @param prompt 提示词 - * @return - */ - @GetMapping("/text2img") - public String text2Image(@RequestParam(value = "prompt") String prompt) { - // 构建文生图参数 - ImageSynthesisParam param = ImageSynthesisParam.builder() - .apiKey(apiKey) // 阿里百炼 API Key - .model("wanx2.1-t2i-plus") // 模型名称 - .prompt(prompt) // 提示词 - .n(1) // 生成图片的数量,这里指定为一张 - .size("1024*1024") // 输出图像的分辨率 - .build(); - - // 同步调用 AI 大模型,生成图片 - ImageSynthesis imageSynthesis = new ImageSynthesis(); - ImageSynthesisResult result = null; - try { - log.info("## 同步调用,请稍等一会..."); - result = imageSynthesis.call(param); - } catch (ApiException | NoApiKeyException e){ - log.error("", e); - } - - // 返回生成的结果(包含图片的 URL 链接) - return JsonUtils.toJson(result); - } - -} \ No newline at end of file diff --git a/src/main/java/com/hanserwei/airobot/controller/ZhiPuController.java b/src/main/java/com/hanserwei/airobot/controller/ZhiPuController.java deleted file mode 100644 index 3a420f2..0000000 --- a/src/main/java/com/hanserwei/airobot/controller/ZhiPuController.java +++ /dev/null @@ -1,47 +0,0 @@ -package com.hanserwei.airobot.controller; - -import jakarta.annotation.Resource; -import org.springframework.ai.chat.messages.UserMessage; -import org.springframework.ai.chat.prompt.Prompt; -import org.springframework.ai.zhipuai.ZhiPuAiChatModel; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; -import reactor.core.publisher.Flux; - -@RestController -@RequestMapping("/v4/ai") -public class ZhiPuController { - - @Resource - private ZhiPuAiChatModel chatModel; - - /** - * 普通对话 - * @param message 对话输入内容 - * @return 对话结果 - */ - @GetMapping("/generate") - public String generate(@RequestParam(value = "message", defaultValue = "你是谁?") String message) { - // 一次性返回结果 - return chatModel.call(message); - } - - /** - * 流式对话 - * @param message 对话输入内容 - * @return 对话结果 - */ - @GetMapping(value = "/generateStream", produces = "text/html;charset=utf-8") - public Flux generateStream(@RequestParam(value = "message", defaultValue = "你是谁?") String message) { - // 构建提示词 - Prompt prompt = new Prompt(new UserMessage(message)); - - // 流式输出 - return chatModel.stream(prompt) - .mapNotNull(chatResponse -> chatResponse.getResult().getOutput().getText()); - - } - -} \ No newline at end of file diff --git a/src/main/java/com/hanserwei/airobot/domain/dos/ChatDO.java b/src/main/java/com/hanserwei/airobot/domain/dos/ChatDO.java new file mode 100644 index 0000000..4cc6547 --- /dev/null +++ b/src/main/java/com/hanserwei/airobot/domain/dos/ChatDO.java @@ -0,0 +1,26 @@ +package com.hanserwei.airobot.domain.dos; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@TableName("t_chat") +public class ChatDO { + + @TableId(type = IdType.AUTO) + private Long id; + private String uuid; + private String summary; + private LocalDateTime createTime; + private LocalDateTime updateTime; +} \ No newline at end of file diff --git a/src/main/java/com/hanserwei/airobot/domain/dos/ChatMessageDO.java b/src/main/java/com/hanserwei/airobot/domain/dos/ChatMessageDO.java new file mode 100644 index 0000000..289a68f --- /dev/null +++ b/src/main/java/com/hanserwei/airobot/domain/dos/ChatMessageDO.java @@ -0,0 +1,26 @@ +package com.hanserwei.airobot.domain.dos; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@TableName("t_chat_message") +public class ChatMessageDO { + + @TableId(type = IdType.AUTO) + private Long id; + private String chatUuid; + private String content; + private String role; + private LocalDateTime createTime; +} \ No newline at end of file diff --git a/src/main/java/com/hanserwei/airobot/domain/mapper/ChatMapper.java b/src/main/java/com/hanserwei/airobot/domain/mapper/ChatMapper.java new file mode 100644 index 0000000..adf6c94 --- /dev/null +++ b/src/main/java/com/hanserwei/airobot/domain/mapper/ChatMapper.java @@ -0,0 +1,7 @@ +package com.hanserwei.airobot.domain.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.hanserwei.airobot.domain.dos.ChatDO; + +public interface ChatMapper extends BaseMapper { +} \ No newline at end of file diff --git a/src/main/java/com/hanserwei/airobot/domain/mapper/ChatMessageMapper.java b/src/main/java/com/hanserwei/airobot/domain/mapper/ChatMessageMapper.java new file mode 100644 index 0000000..e0e2f92 --- /dev/null +++ b/src/main/java/com/hanserwei/airobot/domain/mapper/ChatMessageMapper.java @@ -0,0 +1,8 @@ +package com.hanserwei.airobot.domain.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.hanserwei.airobot.domain.dos.ChatMessageDO; + +public interface ChatMessageMapper extends BaseMapper { + +} \ No newline at end of file diff --git a/src/main/java/com/hanserwei/airobot/enums/ResponseCodeEnum.java b/src/main/java/com/hanserwei/airobot/enums/ResponseCodeEnum.java new file mode 100644 index 0000000..93e609d --- /dev/null +++ b/src/main/java/com/hanserwei/airobot/enums/ResponseCodeEnum.java @@ -0,0 +1,25 @@ +package com.hanserwei.airobot.enums; + +import com.hanserwei.airobot.exception.BaseExceptionInterface; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum ResponseCodeEnum implements BaseExceptionInterface { + + // ----------- 通用异常状态码 ----------- + SYSTEM_ERROR("10000", "出错啦,后台小哥正在努力修复中..."), + PARAM_NOT_VALID("10001", "参数错误"), + + + // ----------- 业务异常状态码 ----------- + // TODO 待填充 + ; + + // 异常码 + private final String errorCode; + // 错误信息 + private final String errorMessage; + +} \ No newline at end of file diff --git a/src/main/java/com/hanserwei/airobot/exception/BaseExceptionInterface.java b/src/main/java/com/hanserwei/airobot/exception/BaseExceptionInterface.java new file mode 100644 index 0000000..45e0607 --- /dev/null +++ b/src/main/java/com/hanserwei/airobot/exception/BaseExceptionInterface.java @@ -0,0 +1,7 @@ +package com.hanserwei.airobot.exception; + +public interface BaseExceptionInterface { + String getErrorCode(); + + String getErrorMessage(); +} \ No newline at end of file diff --git a/src/main/java/com/hanserwei/airobot/exception/BizException.java b/src/main/java/com/hanserwei/airobot/exception/BizException.java new file mode 100644 index 0000000..e37798f --- /dev/null +++ b/src/main/java/com/hanserwei/airobot/exception/BizException.java @@ -0,0 +1,18 @@ +package com.hanserwei.airobot.exception; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class BizException extends RuntimeException { + // 异常码 + private String errorCode; + // 错误信息 + private String errorMessage; + + public BizException(BaseExceptionInterface baseExceptionInterface) { + this.errorCode = baseExceptionInterface.getErrorCode(); + this.errorMessage = baseExceptionInterface.getErrorMessage(); + } +} \ No newline at end of file diff --git a/src/main/java/com/hanserwei/airobot/exception/GlobalExceptionHandler.java b/src/main/java/com/hanserwei/airobot/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..81eefdd --- /dev/null +++ b/src/main/java/com/hanserwei/airobot/exception/GlobalExceptionHandler.java @@ -0,0 +1,90 @@ +package com.hanserwei.airobot.exception; + +import com.hanserwei.airobot.enums.ResponseCodeEnum; +import com.hanserwei.airobot.utils.Response; +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; +import org.springframework.validation.BindingResult; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseBody; + +import java.util.Optional; + +/** + * 全局异常处理器,用于统一处理系统中抛出的各类异常,并返回格式化的错误响应。 + */ +@ControllerAdvice +@Slf4j +public class GlobalExceptionHandler { + + /** + * 捕获自定义业务异常 BizException,并记录警告日志,返回对应的失败响应。 + * + * @param request 当前HTTP请求对象 + * @param e 抛出的业务异常对象 + * @return 返回封装后的失败响应对象 + */ + @ExceptionHandler({ BizException.class }) + @ResponseBody + public Response handleBizException(HttpServletRequest request, BizException e) { + log.warn("{} request fail, errorCode: {}, errorMessage: {}", request.getRequestURI(), e.getErrorCode(), e.getErrorMessage()); + return Response.fail(e); + } + + /** + * 捕获参数校验异常 MethodArgumentNotValidException,提取字段校验错误信息并组合成可读性较强的错误描述, + * 记录警告日志后返回参数校验失败的响应。 + * + * @param request 当前HTTP请求对象 + * @param e 参数校验异常对象 + * @return 返回封装后的参数校验失败响应对象 + */ + @ExceptionHandler({ MethodArgumentNotValidException.class }) + @ResponseBody + public Response handleMethodArgumentNotValidException(HttpServletRequest request, MethodArgumentNotValidException e) { + // 参数错误异常码 + String errorCode = ResponseCodeEnum.PARAM_NOT_VALID.getErrorCode(); + + // 获取 BindingResult + BindingResult bindingResult = e.getBindingResult(); + + StringBuilder sb = new StringBuilder(); + + // 获取校验不通过的字段,并组合错误信息,格式为: email 邮箱格式不正确, 当前值: '123124qq.com'; + Optional.of(bindingResult.getFieldErrors()).ifPresent(errors -> { + errors.forEach(error -> + sb.append(error.getField()) + .append(" ") + .append(error.getDefaultMessage()) + .append(", 当前值: '") + .append(error.getRejectedValue()) + .append("'; ") + + ); + }); + + // 错误信息 + String errorMessage = sb.toString(); + + log.warn("{} request error, errorCode: {}, errorMessage: {}", request.getRequestURI(), errorCode, errorMessage); + + return Response.fail(errorCode, errorMessage); + } + + + /** + * 捕获其他未被处理的异常类型,记录错误日志并返回系统内部错误的响应。 + * + * @param request 当前HTTP请求对象 + * @param e 抛出的异常对象 + * @return 返回封装后的系统错误响应对象 + */ + @ExceptionHandler({ Exception.class }) + @ResponseBody + public Response handleOtherException(HttpServletRequest request, Exception e) { + log.error("{} request error, ", request.getRequestURI(), e); + return Response.fail(ResponseCodeEnum.SYSTEM_ERROR); + } +} diff --git a/src/main/java/com/hanserwei/airobot/model/ActorFilmography.java b/src/main/java/com/hanserwei/airobot/model/ActorFilmography.java deleted file mode 100644 index 1ff7866..0000000 --- a/src/main/java/com/hanserwei/airobot/model/ActorFilmography.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.hanserwei.airobot.model; - -import com.fasterxml.jackson.annotation.JsonPropertyOrder; - -import java.util.List; - -@JsonPropertyOrder({"actor", "movies"}) -public record ActorFilmography(String actor, List movies) { -} \ No newline at end of file diff --git a/src/main/java/com/hanserwei/airobot/model/Book.java b/src/main/java/com/hanserwei/airobot/model/Book.java deleted file mode 100644 index 620ed46..0000000 --- a/src/main/java/com/hanserwei/airobot/model/Book.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.hanserwei.airobot.model; - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.util.List; - -@Data -@AllArgsConstructor -@NoArgsConstructor -public class Book { - /** - * 书名 - */ - private String title; - - /** - * 作者 - */ - private String author; - - /** - * 发布年份 - */ - private Integer publishYear; - - /** - * 类型 - */ - private List genres; - - /** - * 简介 - */ - private String description; -} \ No newline at end of file diff --git a/src/main/java/com/hanserwei/airobot/model/vo/chat/AiChatReqVO.java b/src/main/java/com/hanserwei/airobot/model/vo/chat/AiChatReqVO.java new file mode 100644 index 0000000..28b0cd3 --- /dev/null +++ b/src/main/java/com/hanserwei/airobot/model/vo/chat/AiChatReqVO.java @@ -0,0 +1,35 @@ +package com.hanserwei.airobot.model.vo.chat; + +import jakarta.validation.constraints.NotBlank; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class AiChatReqVO { + + @NotBlank(message = "用户消息不能为空") + private String message; + + /** + * 对话 ID + */ + private String chatId; + + /** + * 联网搜索 + */ + private Boolean networkSearch = false; + + @NotBlank(message = "调用的 AI 大模型名称不能为空") + private String modelName; + + /** + * 温度值,默认为 0.7 + */ + private Double temperature = 0.7; +} \ No newline at end of file diff --git a/src/main/java/com/hanserwei/airobot/model/AIResponse.java b/src/main/java/com/hanserwei/airobot/model/vo/chat/AiResponse.java similarity index 63% rename from src/main/java/com/hanserwei/airobot/model/AIResponse.java rename to src/main/java/com/hanserwei/airobot/model/vo/chat/AiResponse.java index 2a53df5..97839b4 100644 --- a/src/main/java/com/hanserwei/airobot/model/AIResponse.java +++ b/src/main/java/com/hanserwei/airobot/model/vo/chat/AiResponse.java @@ -1,4 +1,4 @@ -package com.hanserwei.airobot.model; +package com.hanserwei.airobot.model.vo.chat; import lombok.AllArgsConstructor; import lombok.Builder; @@ -6,10 +6,12 @@ import lombok.Data; import lombok.NoArgsConstructor; @Data -@Builder @AllArgsConstructor @NoArgsConstructor -public class AIResponse { - // 流式响应内容 +@Builder +public class AiResponse { + /** + * 响应内容 + */ private String v; -} \ No newline at end of file +} diff --git a/src/main/java/com/hanserwei/airobot/model/vo/chat/NewChatReqVO.java b/src/main/java/com/hanserwei/airobot/model/vo/chat/NewChatReqVO.java new file mode 100644 index 0000000..4930677 --- /dev/null +++ b/src/main/java/com/hanserwei/airobot/model/vo/chat/NewChatReqVO.java @@ -0,0 +1,18 @@ +package com.hanserwei.airobot.model.vo.chat; + +import jakarta.validation.constraints.NotBlank; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class NewChatReqVO { + + @NotBlank(message = "用户消息不能为空") + private String message; + +} \ No newline at end of file diff --git a/src/main/java/com/hanserwei/airobot/model/vo/chat/NewChatRspVO.java b/src/main/java/com/hanserwei/airobot/model/vo/chat/NewChatRspVO.java new file mode 100644 index 0000000..7bff80c --- /dev/null +++ b/src/main/java/com/hanserwei/airobot/model/vo/chat/NewChatRspVO.java @@ -0,0 +1,22 @@ +package com.hanserwei.airobot.model.vo.chat; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class NewChatRspVO { + /** + * 摘要 + */ + private String summary; + + /** + * 对话 UUID + */ + private String uuid; +} \ No newline at end of file diff --git a/src/main/java/com/hanserwei/airobot/service/ChatService.java b/src/main/java/com/hanserwei/airobot/service/ChatService.java new file mode 100644 index 0000000..de96f07 --- /dev/null +++ b/src/main/java/com/hanserwei/airobot/service/ChatService.java @@ -0,0 +1,15 @@ +package com.hanserwei.airobot.service; + +import com.hanserwei.airobot.model.vo.chat.NewChatReqVO; +import com.hanserwei.airobot.model.vo.chat.NewChatRspVO; +import com.hanserwei.airobot.utils.Response; + +public interface ChatService { + + /** + * 新建对话 + * @param newChatReqVO 新建对话请求参数 + * @return 新建对话结果 + */ + Response newChat(NewChatReqVO newChatReqVO); +} \ No newline at end of file diff --git a/src/main/java/com/hanserwei/airobot/service/impl/ChatServiceImpl.java b/src/main/java/com/hanserwei/airobot/service/impl/ChatServiceImpl.java new file mode 100644 index 0000000..244c8f6 --- /dev/null +++ b/src/main/java/com/hanserwei/airobot/service/impl/ChatServiceImpl.java @@ -0,0 +1,45 @@ +package com.hanserwei.airobot.service.impl; + +import com.hanserwei.airobot.domain.dos.ChatDO; +import com.hanserwei.airobot.domain.mapper.ChatMapper; +import com.hanserwei.airobot.model.vo.chat.NewChatReqVO; +import com.hanserwei.airobot.model.vo.chat.NewChatRspVO; +import com.hanserwei.airobot.service.ChatService; +import com.hanserwei.airobot.utils.Response; +import com.hanserwei.airobot.utils.StringUtil; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.UUID; + +@Service +public class ChatServiceImpl implements ChatService { + + @Resource + private ChatMapper chatMapper; + + @Override + public Response newChat(NewChatReqVO newChatReqVO) { + // 用户发来的消息 + String message = newChatReqVO.getMessage(); + + // 生成对话的UUID + String uuid = UUID.randomUUID().toString(); + // 截取用户发送的消息,作为对话的摘要 + String summary = StringUtil.truncate(message, 20); + // 存储对话记录到数据库中 + chatMapper.insert(ChatDO.builder() + .summary(summary) + .uuid(uuid) + .createTime(LocalDateTime.now()) + .updateTime(LocalDateTime.now()) + .build()); + + // 将摘要、UUID 返回给前端 + return Response.success(NewChatRspVO.builder() + .uuid(uuid) + .summary(summary) + .build()); + } +} diff --git a/src/main/java/com/hanserwei/airobot/utils/EncryptorUtil.java b/src/main/java/com/hanserwei/airobot/utils/EncryptorUtil.java deleted file mode 100644 index e3909c3..0000000 --- a/src/main/java/com/hanserwei/airobot/utils/EncryptorUtil.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.hanserwei.airobot.utils; - -import org.jasypt.util.text.AES256TextEncryptor; - -public class EncryptorUtil { - public static void main(String[] args) { - AES256TextEncryptor textEncryptor = new AES256TextEncryptor(); - textEncryptor.setPassword("hanserwei"); - System.out.println(textEncryptor.encrypt("sk-QXBlsyIonybNTcG5tt5GvmMpg2WpdMLPTvU55TXrt9urWpL8")); - } -} diff --git a/src/main/java/com/hanserwei/airobot/utils/JsonUtil.java b/src/main/java/com/hanserwei/airobot/utils/JsonUtil.java new file mode 100644 index 0000000..fdb6519 --- /dev/null +++ b/src/main/java/com/hanserwei/airobot/utils/JsonUtil.java @@ -0,0 +1,124 @@ +package com.hanserwei.airobot.utils; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.type.CollectionType; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import lombok.SneakyThrows; +import org.apache.commons.lang3.StringUtils; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * JSON 工具类,提供对象与 JSON 字符串之间的相互转换功能。 + * 支持普通对象、List、Set、Map 等结构的序列化和反序列化。 + */ +public class JsonUtil { + + private static ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + + static { + // 忽略未知属性,防止反序列化失败 + OBJECT_MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + // 允许序列化空对象 + OBJECT_MAPPER.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false); + // 注册 JavaTimeModule 以支持 LocalDateTime 的序列化 + OBJECT_MAPPER.registerModules(new JavaTimeModule()); + } + + /** + * 初始化:统一使用 Spring Boot 个性化配置的 ObjectMapper + * + * @param objectMapper 外部传入的 ObjectMapper 实例 + */ + public static void init(ObjectMapper objectMapper) { + OBJECT_MAPPER = objectMapper; + } + + /** + * 将对象转换为 JSON 字符串 + * + * @param obj 待转换的对象 + * @return 转换后的 JSON 字符串 + */ + @SneakyThrows + public static String toJsonString(Object obj) { + return OBJECT_MAPPER.writeValueAsString(obj); + } + + /** + * 将 JSON 字符串转换为指定类型的对象 + * + * @param jsonStr JSON 字符串 + * @param clazz 目标对象的类类型 + * @param 泛型参数,表示目标对象的类型 + * @return 转换后的对象实例,如果输入为空则返回 null + */ + @SneakyThrows + public static T parseObject(String jsonStr, Class clazz) { + if (StringUtils.isBlank(jsonStr)) { + return null; + } + + return OBJECT_MAPPER.readValue(jsonStr, clazz); + } + + /** + * 将 JSON 字符串转换为指定键值类型的 Map 对象 + * + * @param jsonStr JSON 字符串 + * @param keyClass Map 键的类型 + * @param valueClass Map 值的类型 + * @param 泛型参数,表示 Map 键的类型 + * @param 泛型参数,表示 Map 值的类型 + * @return 转换后的 Map 实例 + * @throws Exception 当解析失败时抛出异常 + */ + public static Map parseMap(String jsonStr, Class keyClass, Class valueClass) throws Exception { + // 构造 Map 类型并进行反序列化 + return OBJECT_MAPPER.readValue(jsonStr, OBJECT_MAPPER.getTypeFactory().constructMapType(Map.class, keyClass, valueClass)); + } + + /** + * 将 JSON 字符串解析为指定元素类型的 List 对象 + * + * @param jsonStr JSON 字符串 + * @param clazz List 中元素的类型 + * @param 泛型参数,表示 List 元素的类型 + * @return 转换后的 List 实例 + * @throws Exception 当解析失败时抛出异常 + */ + public static List parseList(String jsonStr, Class clazz) throws Exception { + // 构造 List 类型并进行反序列化 + return OBJECT_MAPPER.readValue(jsonStr, new TypeReference>() { + @Override + public CollectionType getType() { + return OBJECT_MAPPER.getTypeFactory().constructCollectionType(List.class, clazz); + } + }); + } + + /** + * 将 JSON 字符串解析为指定元素类型的 Set 对象 + * + * @param jsonStr JSON 字符串 + * @param clazz Set 中元素的类型 + * @param 泛型参数,表示 Set 元素的类型 + * @return 转换后的 Set 实例 + * @throws Exception 当解析失败时抛出异常 + */ + public static Set parseSet(String jsonStr, Class clazz) throws Exception { + // 构造 Set 类型并进行反序列化 + return OBJECT_MAPPER.readValue(jsonStr, new TypeReference<>() { + @Override + public CollectionType getType() { + return OBJECT_MAPPER.getTypeFactory().constructCollectionType(Set.class, clazz); + } + }); + } + +} diff --git a/src/main/java/com/hanserwei/airobot/utils/Response.java b/src/main/java/com/hanserwei/airobot/utils/Response.java new file mode 100644 index 0000000..7ddad38 --- /dev/null +++ b/src/main/java/com/hanserwei/airobot/utils/Response.java @@ -0,0 +1,70 @@ +package com.hanserwei.airobot.utils; + +import com.hanserwei.airobot.exception.BaseExceptionInterface; +import com.hanserwei.airobot.exception.BizException; +import lombok.Data; + +import java.io.Serializable; + +@Data +public class Response implements Serializable { + + // 是否成功,默认为 true + private boolean success = true; + // 响应消息 + private String message; + // 异常码 + private String errorCode; + // 响应数据 + private T data; + + // =================================== 成功响应 =================================== + public static Response success() { + return new Response<>(); + } + + public static Response success(T data) { + Response response = new Response<>(); + response.setData(data); + return response; + } + + // =================================== 失败响应 =================================== + public static Response fail() { + Response response = new Response<>(); + response.setSuccess(false); + return response; + } + + public static Response fail(String errorMessage) { + Response response = new Response<>(); + response.setSuccess(false); + response.setMessage(errorMessage); + return response; + } + + public static Response fail(String errorCode, String errorMessage) { + Response response = new Response<>(); + response.setSuccess(false); + response.setErrorCode(errorCode); + response.setMessage(errorMessage); + return response; + } + + public static Response fail(BizException bizException) { + Response response = new Response<>(); + response.setSuccess(false); + response.setErrorCode(bizException.getErrorCode()); + response.setMessage(bizException.getErrorMessage()); + return response; + } + + public static Response fail(BaseExceptionInterface baseExceptionInterface) { + Response response = new Response<>(); + response.setSuccess(false); + response.setErrorCode(baseExceptionInterface.getErrorCode()); + response.setMessage(baseExceptionInterface.getErrorMessage()); + return response; + } + +} \ No newline at end of file diff --git a/src/main/java/com/hanserwei/airobot/utils/StringUtil.java b/src/main/java/com/hanserwei/airobot/utils/StringUtil.java new file mode 100644 index 0000000..0d6cbd6 --- /dev/null +++ b/src/main/java/com/hanserwei/airobot/utils/StringUtil.java @@ -0,0 +1,30 @@ +package com.hanserwei.airobot.utils; + +import org.apache.commons.lang3.StringUtils; + +public class StringUtil { + + /** + * 截取用户问题的前面部分文字作为摘要 + * + * @param message 用户问题 + * @param maxLength 最大截取长度 + * @return 摘要文本,如果原问题长度不足则返回原问题 + */ + public static String truncate(String message, int maxLength) { + // 判空 + if (StringUtils.isBlank(message)) { + return ""; + } + + String trimmed = message.trim(); + + // 如果文本长度小于等于最大长度,直接返回 + if (trimmed.length() <= maxLength) { + return trimmed; + } + + // 截取指定长度 + return trimmed.substring(0, maxLength); + } +} \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index d5b7b08..beff518 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -2,45 +2,32 @@ spring: application: name: han-ai-robot-springboot + datasource: + driver-class-name: com.p6spy.engine.spy.P6SpyDriver # 数据库驱动类名 + url: jdbc:p6spy:postgresql://localhost:5432/han_ai_robot # 数据库连接 URL + username: postgres # 数据库用户名 + password: postgressql # 数据库密码 + hikari: # HikariCP 连接池配置 + pool-name: AI-Robot-HikariCP # 自定义连接池名称 + auto-commit: true # 是否自动提交事务 + connection-timeout: 30000 # 连接超时时间(毫秒) + idle-timeout: 600000 # 空闲连接存活最大时间(毫秒) + max-lifetime: 1800000 # 连接最大存活时间(毫秒) + minimum-idle: 5 # 最小空闲连接数 + maximum-pool-size: 20 # 最大连接池大小 + connection-test-query: SELECT 1 # 连接测试查询 + validation-timeout: 5000 # 验证连接的有效性 cassandra: contact-points: 127.0.0.1 # Cassandra 集群节点地址(可配置多个,用逗号分隔) port: 9042 # 端口号 local-datacenter: datacenter1 # 必须与集群配置的数据中心名称一致(大小写敏感) ai: - deepseek: - api-key: ENC(MROXdiEHmWk08koE63bTzFqW52MaXLpMkM9Cyl40Ubj+Lw1yKeZuHLEcs6jTFY8ditY4gJ1365LMAY8Z9G1uwfYFYaYdb3NyijplX7GuDZA=) # 填写 DeepSeek Api Key, 改成你自己的 - base-url: https://api.deepseek.com # DeepSeek 的请求 URL, 可不填,默认值为 api.deepseek.com - chat: - options: - model: deepseek-chat # 使用哪个模型 - temperature: 0.8 # 温度值 - ollama: - base-url: http://localhost:11434 # Ollama 服务的访问地址, 11434 端口是 Ollama 默认的启动端口 - chat: - options: # 模型参数 - model: qwen3:14b # 指定 Ollama 使用的大模型名称,根据你实际安装的来,我运行的是 14b - temperature: 0.7 # 温度值 - zhipuai: - base-url: https://open.bigmodel.cn/api/paas # 智谱 AI 的请求 URL, 可不填,默认值为 open.bigmodel.cn/api/paas - api-key: ENC(Rz1O0AygSzG3q4UrIpIPHRwFoTQXCUZkWZ54vNzl1kgdBkQECzCYa3LoOADM9NlGLlAwCKTMtkj0nd6cP98T59DohcKtzc3iYyiAoNRfH0rsiu483CpaCciyMwxCUi5O) # 填写智谱 AI 的 API Key, 该成你自己的 - chat: - options: # 模型参数 - model: GLM-4.6 # 模型名称,使用智谱 AI 哪个模型 - temperature: 0.7 # 温度值 - openai: - base-url: https://api.master-jsx.top # OpenAI 服务的访问地址,这里使用的第三方代理商:智增增 - api-key: ENC(D6ETp0VBeDYXvM612dcoGkyHaGUcPuwOVuSLtL92TOCxydyMKXL7/VBndWjFkxAQP/AS7TeQeegla+Ny6TrLStwdJtd28mVhoyf2YsKuXIdRnKF/mv8/uZ0MpzMdv9YR) # 填写智增增的 API Key, 该成你自己的 - chat: - options: - model: gpt-4o # 模型名称 - temperature: 0.7 # 温度值 dashscope: api-key: ENC(cMgcKZkFllyE88DIbGwLKot9Vg02co+gsmY8L8o4/o3UjhcmqO4lJzFU35Sx0n+qFG8pDL0wBjoWrT8X6BuRw9vNlQhY1LgRWHaF9S1zzyM=) chat: options: - model: qwen-omni-turbo - temperature: 0.7 - multi-model: true + model: qwen-plus + temperature: 0.5 chat: memory: repository: @@ -49,7 +36,6 @@ spring: table: t_ai_chat_memory time-to-live: 1095d initialize-schema: true - jasypt: encryptor: password: ${jasypt.encryptor.password} @@ -57,4 +43,5 @@ jasypt: iv-generator-classname: org.jasypt.iv.RandomIvGenerator logging: level: - org.springframework.ai.chat.client.advisor: debug \ No newline at end of file + org.springframework.ai.chat.client.advisor: debug + config: classpath:log4j2.xml # 设置日志配置文件路径 \ No newline at end of file diff --git a/src/main/resources/images/img.png b/src/main/resources/images/img.png deleted file mode 100644 index 29b0848..0000000 Binary files a/src/main/resources/images/img.png and /dev/null differ diff --git a/src/main/resources/log4j2.xml b/src/main/resources/log4j2.xml new file mode 100644 index 0000000..dfb92e1 --- /dev/null +++ b/src/main/resources/log4j2.xml @@ -0,0 +1,94 @@ + + + + + + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n + + ./logs + + %d{yyyy-MM-dd} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/prompts/code-assistant.st b/src/main/resources/prompts/code-assistant.st deleted file mode 100644 index 54018a5..0000000 --- a/src/main/resources/prompts/code-assistant.st +++ /dev/null @@ -1,4 +0,0 @@ -你是一位资深 {lang} 开发工程师。请严格遵循以下要求编写代码: -1. 功能描述:{description} -2. 代码需包含详细注释 -3. 使用业界最佳实践 diff --git a/src/main/resources/spy.properties b/src/main/resources/spy.properties new file mode 100644 index 0000000..490349d --- /dev/null +++ b/src/main/resources/spy.properties @@ -0,0 +1,33 @@ +# 模块列表,根据版本选择合适的配置 +modulelist=com.baomidou.mybatisplus.extension.p6spy.MybatisPlusLogFactory,com.p6spy.engine.outage.P6OutageFactory + +# 自定义日志格式 +logMessageFormat=com.baomidou.mybatisplus.extension.p6spy.P6SpyLogger + +# 日志输出到控制台 +appender=com.baomidou.mybatisplus.extension.p6spy.StdoutLogger + +# 取消JDBC驱动注册 +deregisterdrivers=true + +# 使用前缀 +useprefix=true + +# 排除的日志类别 +excludecategories=info,debug,result,commit,resultset + +# 日期格式 +dateformat=yyyy-MM-dd HH:mm:ss + +# 实际驱动列表 +# driverlist=org.h2.Driver + +# 开启慢SQL记录 +outagedetection=true + +# 慢SQL记录标准(单位:秒) +outagedetectioninterval=2 + +# 过滤 flw_ 开头的表 SQL 打印 +filter=true +exclude=flw_* diff --git a/src/test/java/com/hanserwei/airobot/HanAiRobotSpringbootApplicationTests.java b/src/test/java/com/hanserwei/airobot/HanAiRobotSpringbootApplicationTests.java deleted file mode 100644 index dde31d3..0000000 --- a/src/test/java/com/hanserwei/airobot/HanAiRobotSpringbootApplicationTests.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.hanserwei.airobot; - -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest -class HanAiRobotSpringbootApplicationTests { - - @Test - void contextLoads() { - } - -} diff --git a/src/test/java/com/hanserwei/airobot/MybatisPlusTests.java b/src/test/java/com/hanserwei/airobot/MybatisPlusTests.java new file mode 100644 index 0000000..955670f --- /dev/null +++ b/src/test/java/com/hanserwei/airobot/MybatisPlusTests.java @@ -0,0 +1,31 @@ +package com.hanserwei.airobot; + +import com.hanserwei.airobot.domain.dos.ChatDO; +import com.hanserwei.airobot.domain.mapper.ChatMapper; +import jakarta.annotation.Resource; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +import java.time.LocalDateTime; +import java.util.UUID; + +@SpringBootTest +class MybatisPlusTests { + + @Resource + private ChatMapper chatMapper; + + /** + * 添加数据 + */ + @Test + void testInsert() { + chatMapper.insert(ChatDO.builder() + .uuid(UUID.randomUUID().toString()) + .summary("新对话") + .createTime(LocalDateTime.now()) + .updateTime(LocalDateTime.now()) + .build()); + } + +} \ No newline at end of file