feat(communityCenter): ai窗口
parent
572ea22d4e
commit
a9a2fb5d68
|
@ -0,0 +1,25 @@
|
|||
package com.mcwl.web.controller.communityCenter;
|
||||
|
||||
import com.mcwl.communityCenter.webSocket.ChatWebSocketPoint;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
@RestController
|
||||
@RequestMapping("/chat")
|
||||
public class ChatController {
|
||||
|
||||
private final ChatWebSocketPoint chatWebSocketPoint;
|
||||
|
||||
@GetMapping("/switchUserMode")
|
||||
public void switchUserMode(String sessionId, Boolean isCustomer) throws Exception {
|
||||
if (isCustomer == null) {
|
||||
chatWebSocketPoint.switchUserMode(sessionId, false);
|
||||
} else {
|
||||
chatWebSocketPoint.switchUserMode(sessionId, isCustomer);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -99,6 +99,15 @@ spring:
|
|||
# #连接池最大阻塞等待时间(使用负值表示没有限制)
|
||||
max-wait: -1ms
|
||||
|
||||
#ai配置
|
||||
ai:
|
||||
dashscope:
|
||||
base-url: https://api.deepseek.com/chat/completions
|
||||
api-key: sk-5d1f611b6ba74b90ae9e3dff5aaa508a
|
||||
chat:
|
||||
options:
|
||||
model: deepseek-chat
|
||||
|
||||
|
||||
# token配置
|
||||
token:
|
||||
|
|
|
@ -402,6 +402,12 @@
|
|||
<version>3.19.7</version>
|
||||
</dependency>
|
||||
|
||||
<!-- WebSocket支持 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-websocket</artifactId>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
package com.mcwl.common.config;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
public class JacksonConfig {
|
||||
|
||||
@Bean
|
||||
public ObjectMapper objectMapper() {
|
||||
return new ObjectMapper()
|
||||
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
|
||||
.setSerializationInclusion(JsonInclude.Include.NON_NULL);
|
||||
}
|
||||
}
|
|
@ -31,6 +31,26 @@
|
|||
<groupId>com.mcwl</groupId>
|
||||
<artifactId>mcwl-system</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- https://mvnrepository.com/artifact/com.alibaba/dashscope-sdk-java -->
|
||||
<!-- <dependency>-->
|
||||
<!-- <groupId>com.alibaba</groupId>-->
|
||||
<!-- <artifactId>dashscope-sdk-java</artifactId>-->
|
||||
<!-- <version>2.18.3</version>-->
|
||||
<!-- </dependency>-->
|
||||
<dependency>
|
||||
<groupId>com.alibaba.cloud.ai</groupId>
|
||||
<artifactId>spring-ai-alibaba-starter</artifactId>
|
||||
<version>1.0.0-M5.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.projectreactor.netty</groupId>
|
||||
<artifactId>reactor-netty-http</artifactId>
|
||||
<version>1.1.6</version> <!-- 使用与你的 Spring Boot 版本兼容的版本 -->
|
||||
</dependency>
|
||||
|
||||
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
package com.mcwl.communityCenter.config;
|
||||
|
||||
import com.mcwl.communityCenter.webSocket.ChatWebSocketPoint;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.socket.config.annotation.EnableWebSocket;
|
||||
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
|
||||
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
|
||||
|
||||
@Configuration
|
||||
@EnableWebSocket
|
||||
public class WebSocketConfig implements WebSocketConfigurer {
|
||||
|
||||
@Override
|
||||
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
|
||||
registry.addHandler(chatWebSocketPoint(), "/chat")
|
||||
.setAllowedOrigins("*");
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ChatWebSocketPoint chatWebSocketPoint() {
|
||||
return new ChatWebSocketPoint();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
package com.mcwl.communityCenter.domain;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* DeepSeek API请求参数封装类
|
||||
*/
|
||||
public class DeepSeekRequest {
|
||||
private String model;
|
||||
private List<Message> messages;
|
||||
@JsonProperty("max_tokens")
|
||||
private Integer maxTokens;
|
||||
private Double temperature;
|
||||
private Boolean stream;
|
||||
|
||||
// 构造方法
|
||||
public DeepSeekRequest() {
|
||||
this.messages = new ArrayList<>();
|
||||
}
|
||||
|
||||
// 添加消息的便捷方法
|
||||
public void addMessage(String role, String content) {
|
||||
if (this.messages == null) {
|
||||
this.messages = new ArrayList<>();
|
||||
}
|
||||
this.messages.add(new Message(role, content));
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Getters and Setters
|
||||
// 注意:必须包含所有需要序列化的字段的getter方法
|
||||
|
||||
public static class Message {
|
||||
private String role;
|
||||
private String content;
|
||||
|
||||
public Message(String role, String content) {
|
||||
this.role = role;
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
// Getters
|
||||
public String getRole() { return role; }
|
||||
public String getContent() { return content; }
|
||||
}
|
||||
|
||||
// 以下是各字段的getter/setter
|
||||
public String getModel() { return model; }
|
||||
public void setModel(String model) { this.model = model; }
|
||||
|
||||
public List<Message> getMessages() { return messages; }
|
||||
public void setMessages(List<Message> messages) { this.messages = messages; }
|
||||
|
||||
public Integer getMaxTokens() { return maxTokens; }
|
||||
public void setMaxTokens(Integer maxTokens) { this.maxTokens = maxTokens; }
|
||||
|
||||
public Double getTemperature() { return temperature; }
|
||||
public void setTemperature(Double temperature) { this.temperature = temperature; }
|
||||
public Boolean getStream() {
|
||||
return this.stream;
|
||||
}
|
||||
public void setStream(boolean stream) {
|
||||
this.stream = stream;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
package com.mcwl.communityCenter.service;
|
||||
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
public interface AIService {
|
||||
|
||||
Flux<String> getDeepSeekResponseStream(String message);
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
package com.mcwl.communityCenter.service;
|
||||
|
||||
import org.springframework.web.socket.WebSocketSession;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public interface CustomerService {
|
||||
void transferToHuman(String userId, WebSocketSession session);
|
||||
|
||||
void handleCustomerMessage(String userId, String message) throws IOException;
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
package com.mcwl.communityCenter.service.impl;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.mcwl.common.utils.StringUtils;
|
||||
import com.mcwl.communityCenter.domain.DeepSeekRequest;
|
||||
import com.mcwl.communityCenter.service.AIService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.publisher.SignalType;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class AIServiceImpl implements AIService {
|
||||
|
||||
@Value("${spring.ai.dashscope.base-url}")
|
||||
private String DEEPSEEK_API_URL;
|
||||
@Value("${spring.ai.dashscope.api-key}")
|
||||
private String API_KEY;
|
||||
@Value("${spring.ai.dashscope.chat.options.model}")
|
||||
private String apiModel;
|
||||
|
||||
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
@Override
|
||||
public Flux<String> getDeepSeekResponseStream(String message) {
|
||||
WebClient client = WebClient.builder()
|
||||
.baseUrl(DEEPSEEK_API_URL)
|
||||
.defaultHeader("Authorization", "Bearer " + API_KEY)
|
||||
.build();
|
||||
|
||||
// 构建请求体(推荐使用对象映射)
|
||||
DeepSeekRequest request = new DeepSeekRequest();
|
||||
request.setModel(apiModel);
|
||||
// 添加对话历史
|
||||
request.addMessage("user", message);
|
||||
request.setMaxTokens(500);
|
||||
request.setTemperature(0.7);
|
||||
request.setStream(true);
|
||||
|
||||
return client.post()
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.bodyValue(request)
|
||||
.retrieve()
|
||||
.bodyToFlux(String.class) // 原始数据流
|
||||
.takeUntil(data -> data.contains("[DONE]")) // 遇到结束标记停止
|
||||
.flatMap(json -> parseContentFromJson(json)) // 解析内容
|
||||
.onErrorResume(e -> Flux.just("服务暂时不可用"));// 错误处理
|
||||
|
||||
}
|
||||
|
||||
|
||||
// 辅助方法:从 JSON 中提取 content
|
||||
private Mono<String> parseContentFromJson(String json) {
|
||||
try {
|
||||
JsonNode root = objectMapper.readTree(json);
|
||||
String reasoning_content = root.path("choices")
|
||||
.get(0)
|
||||
.path("delta")
|
||||
.path("reasoning_content")
|
||||
.asText("");
|
||||
String content = root.path("choices")
|
||||
.get(0)
|
||||
.path("delta")
|
||||
.path("content")
|
||||
.asText("");
|
||||
System.out.print(StringUtils.isNotEmpty(reasoning_content) ? reasoning_content : content);
|
||||
return Mono.just(StringUtils.isNotEmpty(reasoning_content) ? reasoning_content : content);
|
||||
} catch (JsonProcessingException e) {
|
||||
return Mono.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
package com.mcwl.communityCenter.service.impl;
|
||||
|
||||
import com.mcwl.communityCenter.service.CustomerService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.socket.TextMessage;
|
||||
import org.springframework.web.socket.WebSocketSession;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
@Service
|
||||
public class CustomerServiceImpl implements CustomerService {
|
||||
|
||||
private static final Map<String, WebSocketSession> activeSessions = new ConcurrentHashMap<>();
|
||||
|
||||
@Override
|
||||
public void transferToHuman(String userId, WebSocketSession session) {
|
||||
activeSessions.put(userId, session);
|
||||
// 这里可以添加通知客服人员的逻辑
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCustomerMessage(String userId, String message) throws IOException {
|
||||
// 应实现消息队列或持久化存储
|
||||
// 先返回固定响应
|
||||
WebSocketSession session = activeSessions.get(userId);
|
||||
if (session != null && session.isOpen()) {
|
||||
session.sendMessage(new TextMessage("[客服] 您好,当前为人工服务模式"));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,92 @@
|
|||
package com.mcwl.communityCenter.webSocket;
|
||||
|
||||
import com.mcwl.communityCenter.service.AIService;
|
||||
import com.mcwl.communityCenter.service.CustomerService;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.socket.CloseStatus;
|
||||
import org.springframework.web.socket.TextMessage;
|
||||
import org.springframework.web.socket.WebSocketSession;
|
||||
import org.springframework.web.socket.handler.AbstractWebSocketHandler;
|
||||
import reactor.core.Disposable;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import javax.websocket.server.ServerEndpoint;
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
@ServerEndpoint("/chat")
|
||||
@NoArgsConstructor
|
||||
public class ChatWebSocketPoint extends AbstractWebSocketHandler {
|
||||
private final Map<String, Boolean> userModes = new ConcurrentHashMap<>();
|
||||
|
||||
// 存储会话与订阅的映射关系
|
||||
private final Map<String, Disposable> sessionSubscriptions = new ConcurrentHashMap<>();
|
||||
|
||||
@Autowired
|
||||
private AIService aiService;
|
||||
|
||||
@Autowired
|
||||
private CustomerService customerService;
|
||||
|
||||
// 构造函数注入服务...
|
||||
|
||||
@Override
|
||||
public void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
|
||||
String userId = session.getId();
|
||||
String userMessage = message.getPayload();
|
||||
// 判断当前模式
|
||||
if (userModes.getOrDefault(userId, false)) {
|
||||
// 人工模式
|
||||
customerService.handleCustomerMessage(userId, userMessage);
|
||||
} else {
|
||||
// AI 流式响应模式
|
||||
Flux<String> responseStream = aiService.getDeepSeekResponseStream(userMessage);
|
||||
|
||||
// 订阅响应流并存储 Disposable
|
||||
Disposable disposable = responseStream
|
||||
.doOnNext(chunk -> sendText(session, chunk))
|
||||
.doOnComplete(() -> sendText(session, "[END]"))
|
||||
.doOnError(e -> sendText(session, "[ERROR] " + e.getMessage()))
|
||||
.subscribe();
|
||||
|
||||
sessionSubscriptions.put(userId, disposable);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
|
||||
super.afterConnectionEstablished(session);
|
||||
userModes.put(session.getId(), false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
|
||||
// 清理订阅资源
|
||||
String sessionId = session.getId();
|
||||
Disposable disposable = sessionSubscriptions.remove(sessionId);
|
||||
if (disposable != null && !disposable.isDisposed()) {
|
||||
disposable.dispose();
|
||||
System.out.println("已清理会话资源: " + sessionId);
|
||||
}
|
||||
}
|
||||
|
||||
// 添加模式切换方法(根据业务需求)
|
||||
public void switchUserMode(String sessionId, boolean isHumanMode) {
|
||||
userModes.put(sessionId, isHumanMode);
|
||||
}
|
||||
|
||||
// 线程安全的发送方法
|
||||
private void sendText(WebSocketSession session, String text) {
|
||||
try {
|
||||
if (session.isOpen()) {
|
||||
synchronized (session) { // WebSocketSession 非线程安全
|
||||
session.sendMessage(new TextMessage(text));
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
System.out.println("WebSocket 发送失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,13 +1,16 @@
|
|||
package com.mcwl.framework.config;
|
||||
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.CacheControl;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||
import org.springframework.web.cors.CorsConfiguration;
|
||||
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
||||
import org.springframework.web.filter.CorsFilter;
|
||||
import org.springframework.web.servlet.config.annotation.AsyncSupportConfigurer;
|
||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
@ -48,6 +51,26 @@ public class ResourcesConfig implements WebMvcConfigurer
|
|||
registry.addInterceptor(repeatSubmitInterceptor).addPathPatterns("/**");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
|
||||
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
|
||||
// 核心线程数(根据服务器CPU核心调整)
|
||||
executor.setCorePoolSize(Runtime.getRuntime().availableProcessors() * 2);
|
||||
// 最大线程数
|
||||
executor.setMaxPoolSize(50);
|
||||
// 队列容量
|
||||
executor.setQueueCapacity(1000);
|
||||
// 线程名前缀
|
||||
executor.setThreadNamePrefix("mvc-async-");
|
||||
// 拒绝策略(CallerRunsPolicy:由调用线程处理该任务)
|
||||
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
|
||||
executor.initialize();
|
||||
|
||||
configurer.setTaskExecutor(executor);
|
||||
// 设置异步超时时间(毫秒)
|
||||
configurer.setDefaultTimeout(30_000);
|
||||
}
|
||||
|
||||
/**
|
||||
* 跨域配置
|
||||
*/
|
||||
|
|
Loading…
Reference in New Issue