fix(chat): 优化聊天流式接口异常处理与拦截器分发类型过滤
- talk 接口改为 TEXT_EVENT_STREAM 输出并包装 Flux 异常处理,返回 error/done 事件 - 登录与签名校验拦截器忽略 ASYNC/ERROR 分发,防止二次校验导致上下文丢失 - 更新嵌入模型与 Qdrant 配置,保持与线上环境一致
This commit is contained in:
@@ -2,6 +2,7 @@ package com.yolo.keyborad.interceptor;
|
|||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.yolo.keyborad.utils.SignUtils;
|
import com.yolo.keyborad.utils.SignUtils;
|
||||||
|
import jakarta.servlet.DispatcherType;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||||
@@ -35,6 +36,10 @@ public class SignInterceptor implements HandlerInterceptor {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
|
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 appId = request.getHeader("X-App-Id");
|
||||||
String timestamp = request.getHeader("X-Timestamp");
|
String timestamp = request.getHeader("X-Timestamp");
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ public class LLMConfig {
|
|||||||
this.openAiApi(),
|
this.openAiApi(),
|
||||||
MetadataMode.EMBED,
|
MetadataMode.EMBED,
|
||||||
OpenAiEmbeddingOptions.builder()
|
OpenAiEmbeddingOptions.builder()
|
||||||
.model("text-embedding-v4")
|
.model("qwen/qwen3-embedding-8b")
|
||||||
.dimensions(1536)
|
.dimensions(1536)
|
||||||
.user("user-6")
|
.user("user-6")
|
||||||
.build(),
|
.build(),
|
||||||
|
|||||||
@@ -12,11 +12,11 @@ import org.springframework.context.annotation.Configuration;
|
|||||||
@Configuration
|
@Configuration
|
||||||
public class QdrantClientConfig {
|
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 Integer qdrantPort = 6334;
|
||||||
|
|
||||||
private final String apiKey = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2Nlc3MiOiJtIn0.HX_GxjXCrnhw2DQbMnMFzvDeaHbmNpI2tj2hoUjkvVU";
|
private final String apiKey = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2Nlc3MiOiJtIn0.M9nnZ14QXQqI3sG8JzRzRwq48x9u5g-4MWF2jnxiOHA";
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public QdrantClient qdrantClient() {
|
public QdrantClient qdrantClient() {
|
||||||
|
|||||||
@@ -1,15 +1,18 @@
|
|||||||
package com.yolo.keyborad.config;
|
package com.yolo.keyborad.config;
|
||||||
|
|
||||||
import cn.dev33.satoken.fun.strategy.SaCorsHandleFunction;
|
import cn.dev33.satoken.fun.strategy.SaCorsHandleFunction;
|
||||||
import cn.dev33.satoken.interceptor.SaInterceptor;
|
|
||||||
import cn.dev33.satoken.router.SaHttpMethod;
|
import cn.dev33.satoken.router.SaHttpMethod;
|
||||||
import cn.dev33.satoken.router.SaRouter;
|
import cn.dev33.satoken.router.SaRouter;
|
||||||
import cn.dev33.satoken.stp.StpUtil;
|
import cn.dev33.satoken.stp.StpUtil;
|
||||||
import com.yolo.keyborad.interceptor.SignInterceptor;
|
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.beans.factory.annotation.Value;
|
||||||
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.data.redis.core.StringRedisTemplate;
|
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.InterceptorRegistry;
|
||||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||||
|
|
||||||
@@ -36,7 +39,17 @@ public class SaTokenConfigure implements WebMvcConfigurer {
|
|||||||
@Override
|
@Override
|
||||||
public void addInterceptors(InterceptorRegistry registry) {
|
public void addInterceptors(InterceptorRegistry registry) {
|
||||||
// 注册 Sa-Token 拦截器,校验规则为 StpUtil.checkLogin() 登录校验。
|
// 注册 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("/**")
|
.addPathPatterns("/**")
|
||||||
.excludePathPatterns(getExcludePaths());
|
.excludePathPatterns(getExcludePaths());
|
||||||
appSecretMap.put(appId, appSecret);
|
appSecretMap.put(appId, appSecret);
|
||||||
@@ -48,6 +61,7 @@ public class SaTokenConfigure implements WebMvcConfigurer {
|
|||||||
return new String[]{
|
return new String[]{
|
||||||
// Swagger & Knife4j 相关
|
// Swagger & Knife4j 相关
|
||||||
"/doc.html",
|
"/doc.html",
|
||||||
|
"/error",
|
||||||
"/webjars/**",
|
"/webjars/**",
|
||||||
"/swagger-resources/**",
|
"/swagger-resources/**",
|
||||||
"/v2/api-docs",
|
"/v2/api-docs",
|
||||||
@@ -57,7 +71,6 @@ public class SaTokenConfigure implements WebMvcConfigurer {
|
|||||||
"/swagger-ui/**",
|
"/swagger-ui/**",
|
||||||
"/favicon.ico",
|
"/favicon.ico",
|
||||||
// 你的其他放行路径,例如登录接口
|
// 你的其他放行路径,例如登录接口
|
||||||
"/error",
|
|
||||||
"/user/appleLogin",
|
"/user/appleLogin",
|
||||||
"/user/logout",
|
"/user/logout",
|
||||||
"/tag/list",
|
"/tag/list",
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
|||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.ai.openai.OpenAiEmbeddingModel;
|
import org.springframework.ai.openai.OpenAiEmbeddingModel;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.codec.ServerSentEvent;
|
import org.springframework.http.codec.ServerSentEvent;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import reactor.core.publisher.Flux;
|
import reactor.core.publisher.Flux;
|
||||||
@@ -90,10 +91,22 @@ public class ChatController {
|
|||||||
return ResultUtils.success(result);
|
return ResultUtils.success(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/talk")
|
@PostMapping(value = "/talk", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
|
||||||
@Operation(summary = "聊天润色接口", description = "聊天润色接口")
|
@Operation(summary = "聊天润色接口", description = "聊天润色接口")
|
||||||
public Flux<ServerSentEvent<ChatStreamMessage>> talk(@RequestBody ChatReq chatReq){
|
public Flux<ServerSentEvent<ChatStreamMessage>> 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()
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ spring:
|
|||||||
model: google/gemini-2.5-flash-lite
|
model: google/gemini-2.5-flash-lite
|
||||||
embedding:
|
embedding:
|
||||||
options:
|
options:
|
||||||
model: text-embedding-v4
|
model: qwen/qwen3-embedding-8b
|
||||||
# model: qwen/qwen3-embedding-8b
|
# model: qwen/qwen3-embedding-8b
|
||||||
dashscope:
|
dashscope:
|
||||||
api-key: 11
|
api-key: 11
|
||||||
|
|||||||
Reference in New Issue
Block a user