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