From 590230a86b60c4a0d61208bfa206c762c3c8e239 Mon Sep 17 00:00:00 2001 From: ziin Date: Tue, 24 Feb 2026 14:54:59 +0800 Subject: [PATCH] =?UTF-8?q?fix(chat):=20=E4=BC=98=E5=8C=96=E8=81=8A?= =?UTF-8?q?=E5=A4=A9=E6=B5=81=E5=BC=8F=E6=8E=A5=E5=8F=A3=E5=BC=82=E5=B8=B8?= =?UTF-8?q?=E5=A4=84=E7=90=86=E4=B8=8E=E6=8B=A6=E6=88=AA=E5=99=A8=E5=88=86?= =?UTF-8?q?=E5=8F=91=E7=B1=BB=E5=9E=8B=E8=BF=87=E6=BB=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - talk 接口改为 TEXT_EVENT_STREAM 输出并包装 Flux 异常处理,返回 error/done 事件 - 登录与签名校验拦截器忽略 ASYNC/ERROR 分发,防止二次校验导致上下文丢失 - 更新嵌入模型与 Qdrant 配置,保持与线上环境一致 --- .../keyborad/Interceptor/SignInterceptor.java | 5 +++++ .../com/yolo/keyborad/config/LLMConfig.java | 2 +- .../keyborad/config/QdrantClientConfig.java | 4 ++-- .../keyborad/config/SaTokenConfigure.java | 19 ++++++++++++++++--- .../keyborad/controller/ChatController.java | 17 +++++++++++++++-- src/main/resources/application.yml | 2 +- 6 files changed, 40 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/yolo/keyborad/Interceptor/SignInterceptor.java b/src/main/java/com/yolo/keyborad/Interceptor/SignInterceptor.java index 884f283..e08c7b0 100644 --- a/src/main/java/com/yolo/keyborad/Interceptor/SignInterceptor.java +++ b/src/main/java/com/yolo/keyborad/Interceptor/SignInterceptor.java @@ -2,6 +2,7 @@ package com.yolo.keyborad.interceptor; import com.fasterxml.jackson.databind.ObjectMapper; import com.yolo.keyborad.utils.SignUtils; +import jakarta.servlet.DispatcherType; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.springframework.data.redis.core.StringRedisTemplate; @@ -35,6 +36,10 @@ public class SignInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + // 异步/错误二次分发不重复验签,避免 nonce 重放误判 + if (request.getDispatcherType() != DispatcherType.REQUEST) { + return true; + } String appId = request.getHeader("X-App-Id"); String timestamp = request.getHeader("X-Timestamp"); diff --git a/src/main/java/com/yolo/keyborad/config/LLMConfig.java b/src/main/java/com/yolo/keyborad/config/LLMConfig.java index d566b12..a7abaae 100644 --- a/src/main/java/com/yolo/keyborad/config/LLMConfig.java +++ b/src/main/java/com/yolo/keyborad/config/LLMConfig.java @@ -57,7 +57,7 @@ public class LLMConfig { this.openAiApi(), MetadataMode.EMBED, OpenAiEmbeddingOptions.builder() - .model("text-embedding-v4") + .model("qwen/qwen3-embedding-8b") .dimensions(1536) .user("user-6") .build(), diff --git a/src/main/java/com/yolo/keyborad/config/QdrantClientConfig.java b/src/main/java/com/yolo/keyborad/config/QdrantClientConfig.java index bb0269d..6743f19 100644 --- a/src/main/java/com/yolo/keyborad/config/QdrantClientConfig.java +++ b/src/main/java/com/yolo/keyborad/config/QdrantClientConfig.java @@ -12,11 +12,11 @@ import org.springframework.context.annotation.Configuration; @Configuration public class QdrantClientConfig { - private final String qdrantHost = "b0c7f1ee-0eb9-469e-83e0-654249d9bd04.us-east4-0.gcp.cloud.qdrant.io"; + private final String qdrantHost = "00044004-d9d2-4705-8b7b-9defab34ca1b.us-west-1-0.aws.cloud.qdrant.io"; private final Integer qdrantPort = 6334; - private final String apiKey = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2Nlc3MiOiJtIn0.HX_GxjXCrnhw2DQbMnMFzvDeaHbmNpI2tj2hoUjkvVU"; + private final String apiKey = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2Nlc3MiOiJtIn0.M9nnZ14QXQqI3sG8JzRzRwq48x9u5g-4MWF2jnxiOHA"; @Bean public QdrantClient qdrantClient() { diff --git a/src/main/java/com/yolo/keyborad/config/SaTokenConfigure.java b/src/main/java/com/yolo/keyborad/config/SaTokenConfigure.java index 2cd7a8a..35802b5 100644 --- a/src/main/java/com/yolo/keyborad/config/SaTokenConfigure.java +++ b/src/main/java/com/yolo/keyborad/config/SaTokenConfigure.java @@ -1,15 +1,18 @@ package com.yolo.keyborad.config; import cn.dev33.satoken.fun.strategy.SaCorsHandleFunction; -import cn.dev33.satoken.interceptor.SaInterceptor; import cn.dev33.satoken.router.SaHttpMethod; import cn.dev33.satoken.router.SaRouter; import cn.dev33.satoken.stp.StpUtil; import com.yolo.keyborad.interceptor.SignInterceptor; +import jakarta.servlet.DispatcherType; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @@ -36,7 +39,17 @@ public class SaTokenConfigure implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { // 注册 Sa-Token 拦截器,校验规则为 StpUtil.checkLogin() 登录校验。 - registry.addInterceptor(new SaInterceptor(handle -> StpUtil.checkLogin())) + registry.addInterceptor(new HandlerInterceptor() { + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { + // 仅在首次 REQUEST 分发时做登录校验,避免 ASYNC/ERROR 二次分发阶段上下文丢失 + if (request.getDispatcherType() != DispatcherType.REQUEST) { + return true; + } + StpUtil.checkLogin(); + return true; + } + }) .addPathPatterns("/**") .excludePathPatterns(getExcludePaths()); appSecretMap.put(appId, appSecret); @@ -48,6 +61,7 @@ public class SaTokenConfigure implements WebMvcConfigurer { return new String[]{ // Swagger & Knife4j 相关 "/doc.html", + "/error", "/webjars/**", "/swagger-resources/**", "/v2/api-docs", @@ -57,7 +71,6 @@ public class SaTokenConfigure implements WebMvcConfigurer { "/swagger-ui/**", "/favicon.ico", // 你的其他放行路径,例如登录接口 - "/error", "/user/appleLogin", "/user/logout", "/tag/list", diff --git a/src/main/java/com/yolo/keyborad/controller/ChatController.java b/src/main/java/com/yolo/keyborad/controller/ChatController.java index 764ee77..deb3494 100644 --- a/src/main/java/com/yolo/keyborad/controller/ChatController.java +++ b/src/main/java/com/yolo/keyborad/controller/ChatController.java @@ -30,6 +30,7 @@ import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.ai.openai.OpenAiEmbeddingModel; +import org.springframework.http.MediaType; import org.springframework.http.codec.ServerSentEvent; import org.springframework.web.bind.annotation.*; import reactor.core.publisher.Flux; @@ -90,10 +91,22 @@ public class ChatController { return ResultUtils.success(result); } - @PostMapping("/talk") + @PostMapping(value = "/talk", produces = MediaType.TEXT_EVENT_STREAM_VALUE) @Operation(summary = "聊天润色接口", description = "聊天润色接口") public Flux> talk(@RequestBody ChatReq chatReq){ - return chatService.talk(chatReq); + return Flux.defer(() -> chatService.talk(chatReq)) + .onErrorResume(e -> { + log.error("聊天流式接口异常", e); + String message = StrUtil.isBlank(e.getMessage()) ? "服务暂时不可用,请稍后重试" : e.getMessage(); + return Flux.just( + ServerSentEvent.builder(new ChatStreamMessage("error", message)) + .event("error") + .build(), + ServerSentEvent.builder(new ChatStreamMessage("done", null)) + .event("done") + .build() + ); + }); } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index f50851c..d3e8449 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -10,7 +10,7 @@ spring: model: google/gemini-2.5-flash-lite embedding: options: - model: text-embedding-v4 + model: qwen/qwen3-embedding-8b # model: qwen/qwen3-embedding-8b dashscope: api-key: 11