添加弹幕功能代码

main
gitee 6 days ago
parent 2a8c4d6805
commit d38d33f578

@ -48,6 +48,15 @@
<artifactId>mybatis-plus-boot-starter</artifactId> <artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.5</version> <version>3.5.5</version>
</dependency> </dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>com.hankcs</groupId>
<artifactId>hanlp</artifactId>
<version>portable-1.8.6</version>
</dependency>
<dependency> <dependency>
<groupId>org.postgresql</groupId> <groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId> <artifactId>postgresql</artifactId>

@ -36,7 +36,7 @@ public class SecurityConfig {
http http
.csrf(AbstractHttpConfigurer::disable) // 禁用CSRF .csrf(AbstractHttpConfigurer::disable) // 禁用CSRF
.authorizeHttpRequests(auth -> auth .authorizeHttpRequests(auth -> auth
.requestMatchers("/auth/**","/agent/streamChat").permitAll() .requestMatchers("/auth/**","/agent/streamChat","/livetalking/chatCallBack","/**").permitAll()
.anyRequest().authenticated() .anyRequest().authenticated()
) )
.sessionManagement(session -> session .sessionManagement(session -> session

@ -0,0 +1,22 @@
package com.supervision.config;
import com.supervision.service.danmaku.DanmakuWebSocketHandler;
import lombok.RequiredArgsConstructor;
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
@RequiredArgsConstructor
public class WebSocketConfig implements WebSocketConfigurer {
private final DanmakuWebSocketHandler webSocketHandler;
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(webSocketHandler, "/ws/danmaku")
.setAllowedOrigins("*");
}
}

@ -4,12 +4,14 @@ public class PromptTemplate {
public static final String GENERAL_INDUSTRY_TEMPLATE = """ public static final String GENERAL_INDUSTRY_TEMPLATE = """
{query} {query}
"""; """;
public static final String HEART_GUIDE_TEMPLATE = """ public static final String HEART_GUIDE_TEMPLATE = """
怀 怀
{query} {query}
"""; """;
} }

@ -34,6 +34,12 @@ public class AgentController {
return agentService.streamChat(agentChatReqDTO); return agentService.streamChat(agentChatReqDTO);
} }
/**
*
* @param page
* @param pageSize
* @return
*/
@GetMapping("/pageList") @GetMapping("/pageList")
public R<IPage<AgentInfoDTO>> pageList(@RequestParam (name = "page", required = false,defaultValue = "1") Integer page, public R<IPage<AgentInfoDTO>> pageList(@RequestParam (name = "page", required = false,defaultValue = "1") Integer page,
@RequestParam (name = "pageSize", required = false,defaultValue = "10") Integer pageSize) { @RequestParam (name = "pageSize", required = false,defaultValue = "10") Integer pageSize) {

@ -0,0 +1,52 @@
package com.supervision.controller;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.supervision.domain.LivetalkingChatDTO;
import com.supervision.dto.DigitalHumanDTO;
import com.supervision.dto.DigitalHumanVoiceDTO;
import com.supervision.dto.R;
import com.supervision.service.danmaku.DigitalHumanManageService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
/**
*
*/
@RestController
@RequestMapping("/digitalHuman")
@RequiredArgsConstructor
public class DigitalHumanController {
private final DigitalHumanManageService digitalHumanManageService;
/**
*
* @param page
* @param pageSize
* @return
*/
@GetMapping("/pageList")
public R<IPage<DigitalHumanDTO>> pageList(@RequestParam(name = "page", required = false,defaultValue = "1") Integer page,
@RequestParam (name = "pageSize", required = false,defaultValue = "10") Integer pageSize) {
IPage<DigitalHumanDTO> paged = digitalHumanManageService.pageList(page, pageSize);
return R.ok(paged);
}
/**
*
* @param digitalHumanVoiceDTO IDIDDTO
* @return
*/
@PostMapping("/switchVoice")
public R<Void> setVoice(@RequestBody DigitalHumanVoiceDTO digitalHumanVoiceDTO) {
digitalHumanManageService.setVoice(digitalHumanVoiceDTO);
return R.ok();
}
@PostMapping("/livetalking/chatCallBack")
public R<Void> chatCallBack(@RequestBody LivetalkingChatDTO digitalHumanVoiceDTO) {
digitalHumanManageService.chatCallBack(digitalHumanVoiceDTO);
return R.ok();
}
}

@ -55,6 +55,11 @@ public class DigitalHuman implements Serializable {
*/ */
private String headPicId; private String headPicId;
/**
* 0 1
*/
private String gender;
/** /**
* *
*/ */

@ -0,0 +1,42 @@
package com.supervision.domain;
import com.supervision.dto.DanmakuMessage;
import lombok.Data;
@Data
public class LivetalkingChatDTO {
/**
* id
*/
private String messageId;
/**
* id
*/
private String humanId;
/**
* id
*/
private String roomId;
/**
*
*/
private String query;
/**
*
*/
private String answer;
public LivetalkingChatDTO() {
}
public LivetalkingChatDTO(DanmakuMessage danmakuMessage) {
this.roomId = danmakuMessage.getRoomId();
this.query = danmakuMessage.getContent();
}
}

@ -8,6 +8,8 @@ import java.util.Collection;
public class UserDetail extends User { public class UserDetail extends User {
private String userId; private String userId;
private String nickname;
public UserDetail(String userId ,String username, String password, Collection<? extends GrantedAuthority> authorities) { public UserDetail(String userId ,String username, String password, Collection<? extends GrantedAuthority> authorities) {
super(username, password, authorities); super(username, password, authorities);
@ -29,4 +31,12 @@ public class UserDetail extends User {
public String getUserId() { public String getUserId() {
return userId; return userId;
} }
public String getNickname() {
return nickname;
}
public void setNickname(String nickname) {
this.nickname = nickname;
}
} }

@ -0,0 +1,92 @@
package com.supervision.dto;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.supervision.domain.DigitalHuman;
import com.supervision.domain.DigitalHumanDialogueLog;
import lombok.Data;
import java.lang.ref.PhantomReference;
@Data
public class DanmakuMessage {
/**
* ID
*/
private String roomId;
/**
*
*/
private String content;
/**
* ID
*/
private String userId;
/**
*
*/
private String nickname;
/**
*
*/
private String color;
/**
*
*/
private Integer size;
/**
*
*/
private Long timestamp;
/**
* 0 1
*/
private String type;
public DanmakuMessage() {
}
// 简化构造方法
public DanmakuMessage(String roomId, String content, String userId) {
this.roomId = roomId;
this.content = content;
this.userId = userId;
this.color = "#ffffff";
this.size = 16;
this.timestamp = System.currentTimeMillis();
}
public String toJson() {
try {
return new ObjectMapper().writeValueAsString(this);
} catch (JsonProcessingException e) {
throw new RuntimeException("序列化失败", e);
}
}
public static DanmakuMessage fromJson(String json) {
try {
return new ObjectMapper().readValue(json, DanmakuMessage.class);
} catch (JsonProcessingException e) {
throw new RuntimeException("反序列化失败", e);
}
}
public DigitalHumanDialogueLog toDialogueLog() {
DigitalHumanDialogueLog log = new DigitalHumanDialogueLog();
log.setDigitalHumanId(this.roomId);
log.setUserInput(this.content);
log.setUserId(this.userId);
log.setAnswerType("0"); // 默认回答类型为正常回答
return log;
}
}

@ -0,0 +1,65 @@
package com.supervision.dto;
import com.supervision.domain.DigitalHuman;
import lombok.Data;
@Data
public class DigitalHumanDTO {
private String id;
/**
*
*/
private String modelName;
/**
*
*/
private String industry;
/**
*
*/
private String goodAt;
/**
* id
*/
private String defaultVoiceId;
/**
* 线 0线 1线
*/
private String onlineStatus;
/**
* 0 1
*/
private String modelType;
/**
* id
*/
private String headPicId;
/**
* 0 1
*/
private String gender;
public DigitalHumanDTO() {
}
public DigitalHumanDTO(DigitalHuman digitalHuman) {
this.id = digitalHuman.getId();
this.modelName = digitalHuman.getModelName();
this.industry = digitalHuman.getIndustry();
this.goodAt = digitalHuman.getGoodAt();
this.defaultVoiceId = digitalHuman.getDefaultVoiceId();
this.onlineStatus = digitalHuman.getOnlineStatus();
this.modelType = digitalHuman.getModelType();
this.headPicId = digitalHuman.getHeadPicId();
this.gender = digitalHuman.getGender();
}
}

@ -0,0 +1,12 @@
package com.supervision.dto;
import lombok.Data;
@Data
public class DigitalHumanVoiceDTO {
private String voiceId;
private String digitalHumanId;
}

@ -2,6 +2,7 @@ package com.supervision.service;
import com.supervision.domain.DigitalHumanDialogueLog; import com.supervision.domain.DigitalHumanDialogueLog;
import com.baomidou.mybatisplus.extension.service.IService; import com.baomidou.mybatisplus.extension.service.IService;
import com.supervision.dto.DanmakuMessage;
/** /**
* @author Administrator * @author Administrator
@ -10,4 +11,7 @@ import com.baomidou.mybatisplus.extension.service.IService;
*/ */
public interface DigitalHumanDialogueLogService extends IService<DigitalHumanDialogueLog> { public interface DigitalHumanDialogueLogService extends IService<DigitalHumanDialogueLog> {
String saveLog(DanmakuMessage danmakuMessage);
} }

@ -0,0 +1,15 @@
package com.supervision.service;
import com.supervision.domain.LivetalkingChatDTO;
/**
*
*/
public interface LivetalkingService {
void chat(LivetalkingChatDTO livetalkingChatDTO);
void setVoice(String voiceId, String digitalHumanId);
}

@ -0,0 +1,119 @@
package com.supervision.service.danmaku;
import com.supervision.dto.DanmakuMessage;
import java.util.concurrent.*;
import java.util.function.Predicate;
/**
* Flow API
*/
public class DanmakuPublisher {
// 使用单例模式
private static final DanmakuPublisher INSTANCE = new DanmakuPublisher();
// 使用SubmissionPublisher作为基础实现
private final SubmissionPublisher<DanmakuMessage> publisher;
// 房间过滤器缓存
private final ConcurrentHashMap<String, Predicate<DanmakuMessage>> roomFilters;
// 线程池
private final ExecutorService executor;
private DanmakuPublisher() {
this.executor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
this.publisher = new SubmissionPublisher<>(executor, Flow.defaultBufferSize());
this.roomFilters = new ConcurrentHashMap<>();
}
public static DanmakuPublisher getInstance() {
return INSTANCE;
}
/**
*
*/
public void publish(DanmakuMessage danmaku) {
publisher.submit(danmaku);
}
/**
*
*/
public Flow.Subscription subscribe(String roomId, Flow.Subscriber<DanmakuMessage> subscriber) {
// 创建房间过滤器
Predicate<DanmakuMessage> filter = msg -> roomId.equals(msg.getRoomId());
roomFilters.putIfAbsent(roomId, filter);
// 创建过滤处理器
FilterProcessor processor = new FilterProcessor(filter, executor);
publisher.subscribe(processor);
processor.subscribe(subscriber);
return new DanmakuSubscription(processor);
}
/**
* Subscription
*/
private static class DanmakuSubscription implements Flow.Subscription {
private final FilterProcessor processor;
DanmakuSubscription(FilterProcessor processor) {
this.processor = processor;
}
@Override
public void request(long n) {
// 不实现背压控制
}
@Override
public void cancel() {
processor.cancel();
}
}
/**
* Processor
*/
private static class FilterProcessor extends SubmissionPublisher<DanmakuMessage>
implements Flow.Processor<DanmakuMessage, DanmakuMessage> {
private final Predicate<DanmakuMessage> filter;
private Flow.Subscription subscription;
FilterProcessor(Predicate<DanmakuMessage> filter, ExecutorService executor) {
super(executor, Flow.defaultBufferSize());
this.filter = filter;
}
@Override
public void onSubscribe(Flow.Subscription subscription) {
this.subscription = subscription;
subscription.request(Long.MAX_VALUE); // 不限制请求数量
}
@Override
public void onNext(DanmakuMessage item) {
if (filter.test(item)) {
submit(item); // 转发给订阅者
}
}
@Override
public void onError(Throwable throwable) {
closeExceptionally(throwable);
}
@Override
public void onComplete() {
close();
}
public void cancel() {
if (subscription != null) {
subscription.cancel();
}
close();
}
}
}

@ -0,0 +1,163 @@
package com.supervision.service.danmaku;
import cn.hutool.core.lang.Assert;
import com.hankcs.hanlp.seg.common.Term;
import com.hankcs.hanlp.tokenizer.StandardTokenizer;
import com.supervision.domain.LivetalkingChatDTO;
import com.supervision.domain.UserDetail;
import com.supervision.dto.DanmakuMessage;
import com.supervision.service.DigitalHumanDialogueLogService;
import com.supervision.service.LivetalkingService;
import com.supervision.util.UserUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;
import java.io.IOException;
import java.util.concurrent.Flow;
@Slf4j
@Service
@RequiredArgsConstructor
public class DanmakuWebSocketHandler extends TextWebSocketHandler {
private final WebSocketSessionManager sessionManager;
private final DigitalHumanDialogueLogService dialogueLogService;
private final LivetalkingService livetalkingService;
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage textMessage) {
// 解析客户端消息
DanmakuMessage danmaku = DanmakuMessage.fromJson(textMessage.getPayload());
String roomId = getRoomIdFromSession(session);
danmaku.setRoomId(roomId);
// 验证消息
validateDanmaku(danmaku);
// 保存到日志
String messageId = dialogueLogService.saveLog(danmaku);
// 判断是否需要回复
if (isQuestion(danmaku.getContent())) {
log.info("检测到问题: {}", danmaku.getContent());
LivetalkingChatDTO livetalkingChatDTO = new LivetalkingChatDTO(danmaku);
livetalkingChatDTO.setMessageId(messageId);
livetalkingService.chat(livetalkingChatDTO);
}
// 发布到消息中心
DanmakuPublisher.getInstance().publish(danmaku);
}
@Override
public void afterConnectionEstablished(WebSocketSession session) {
String roomId = getRoomIdFromSession(session);
sessionManager.addSession(roomId, session);
// 为该会话创建订阅者
Flow.Subscriber<DanmakuMessage> subscriber = createSubscriber(session);
Flow.Subscription subscription = DanmakuPublisher.getInstance()
.subscribe(roomId, subscriber);
// 将会话与订阅关系保存,方便在连接关闭时取消订阅
session.getAttributes().put("subscription", subscription);
try {
UserDetail userDetail = UserUtil.currentUser(session);
DanmakuMessage message = new DanmakuMessage();
message.setUserId(userDetail.getUserId());
message.setNickname(userDetail.getNickname());
message.setContent(userDetail.getNickname() + "来了");
DanmakuPublisher.getInstance().publish(message);
}catch (Exception e){
log.error("获取用户信息失败: {}", e.getMessage());
}
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
String roomId = getRoomIdFromSession(session);
sessionManager.removeSession(roomId, session);
// 取消订阅
Flow.Subscription subscription = (Flow.Subscription)
session.getAttributes().get("subscription");
if (subscription != null) {
subscription.cancel();
}
}
private Flow.Subscriber<DanmakuMessage> createSubscriber(WebSocketSession session) {
return new Flow.Subscriber<>() {
private Flow.Subscription subscription;
@Override
public void onSubscribe(Flow.Subscription subscription) {
this.subscription = subscription;
subscription.request(Long.MAX_VALUE); // 不限制请求数量
}
@Override
public void onNext(DanmakuMessage danmaku) {
try {
if (session.isOpen()) {
session.sendMessage(new TextMessage(danmaku.toJson()));
}
} catch (IOException e) {
subscription.cancel();
}
}
@Override
public void onError(Throwable throwable) {
// 处理错误
log.info("WebSocket错误: {}", throwable.getMessage());
}
@Override
public void onComplete() {
// 发布者关闭
log.info("WebSocket连接已关闭: {}", session.getId());
}
};
}
private String getRoomIdFromSession(WebSocketSession session) {
// 从URI获取roomId如 ws://localhost:8080/ws/danmaku?roomId=123
String query = session.getUri().getQuery();
return query.split("=")[1];
}
private void validateDanmaku(DanmakuMessage danmaku) {
// 验证逻辑...
Assert.notEmpty(danmaku.getRoomId(), "房间ID不能为空");
Assert.notEmpty(danmaku.getContent(), "弹幕内容不能为空");
Assert.notEmpty(danmaku.getUserId(), "用户ID不能为空");
}
private boolean isQuestion(String sentence) {
String[] questionWords = {"什么", "为什么", "如何", "哪", "谁", "多少", "是否", "能否", "是不是"};
String[] questionEndings = {"吗", "呢", "", "?"};
// 结尾判断
for (String end : questionEndings) {
if (sentence.trim().endsWith(end)) {
return true;
}
}
// 分词判断
for (Term term : StandardTokenizer.segment(sentence)) {
for (String qw : questionWords) {
if (term.word.equals(qw)) {
return true;
}
}
}
return false;
}
}

@ -0,0 +1,23 @@
package com.supervision.service.danmaku;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.supervision.domain.LivetalkingChatDTO;
import com.supervision.dto.DigitalHumanDTO;
import com.supervision.dto.DigitalHumanVoiceDTO;
public interface DigitalHumanManageService {
/**
*
* @param page
* @param pageSize
* @return
*/
IPage<DigitalHumanDTO> pageList(Integer page, Integer pageSize);
void setVoice(DigitalHumanVoiceDTO digitalHumanVoiceDTO);
void chatCallBack(LivetalkingChatDTO digitalHumanVoiceDTO);
}

@ -0,0 +1,39 @@
package com.supervision.service.danmaku;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.web.socket.WebSocketSession;
import java.util.Collections;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
/**
* WebSocket
*/
@Service
@RequiredArgsConstructor
public class WebSocketSessionManager {
// 房间ID -> 会话列表
private final ConcurrentMap<String, Set<WebSocketSession>> roomSessions = new ConcurrentHashMap<>();
public void addSession(String roomId, WebSocketSession session) {
roomSessions.computeIfAbsent(roomId, k -> ConcurrentHashMap.newKeySet())
.add(session);
}
public void removeSession(String roomId, WebSocketSession session) {
Set<WebSocketSession> sessions = roomSessions.get(roomId);
if (sessions != null) {
sessions.remove(session);
if (sessions.isEmpty()) {
roomSessions.remove(roomId);
}
}
}
public Set<WebSocketSession> getSessionsByRoom(String roomId) {
return roomSessions.getOrDefault(roomId, Collections.emptySet());
}
}

@ -2,6 +2,7 @@ package com.supervision.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.supervision.domain.DigitalHumanDialogueLog; import com.supervision.domain.DigitalHumanDialogueLog;
import com.supervision.dto.DanmakuMessage;
import com.supervision.service.DigitalHumanDialogueLogService; import com.supervision.service.DigitalHumanDialogueLogService;
import com.supervision.mapper.DigitalHumanDialogueLogMapper; import com.supervision.mapper.DigitalHumanDialogueLogMapper;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@ -15,6 +16,12 @@ import org.springframework.stereotype.Service;
public class DigitalHumanDialogueLogServiceImpl extends ServiceImpl<DigitalHumanDialogueLogMapper, DigitalHumanDialogueLog> public class DigitalHumanDialogueLogServiceImpl extends ServiceImpl<DigitalHumanDialogueLogMapper, DigitalHumanDialogueLog>
implements DigitalHumanDialogueLogService{ implements DigitalHumanDialogueLogService{
@Override
public String saveLog(DanmakuMessage danmakuMessage) {
DigitalHumanDialogueLog dialogueLog = danmakuMessage.toDialogueLog();
super.save(dialogueLog);
return dialogueLog.getId();
}
} }

@ -0,0 +1,73 @@
package com.supervision.service.impl;
import cn.hutool.core.lang.Assert;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.supervision.domain.LivetalkingChatDTO;
import com.supervision.domain.DigitalHuman;
import com.supervision.domain.DigitalHumanDialogueLog;
import com.supervision.domain.VoiceInfo;
import com.supervision.dto.DanmakuMessage;
import com.supervision.dto.DigitalHumanDTO;
import com.supervision.dto.DigitalHumanVoiceDTO;
import com.supervision.service.DigitalHumanDialogueLogService;
import com.supervision.service.LivetalkingService;
import com.supervision.service.VoiceInfoService;
import com.supervision.service.danmaku.DanmakuPublisher;
import com.supervision.service.danmaku.DigitalHumanManageService;
import com.supervision.service.DigitalHumanService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@Slf4j
@Service
@RequiredArgsConstructor
public class DigitalHumanManageServiceImpl implements DigitalHumanManageService {
private final DigitalHumanService digitalHumanService;
private final LivetalkingService livetalkingService;
private final VoiceInfoService voiceInfoService;
private final DigitalHumanDialogueLogService dialogueLogService;
@Override
public IPage<DigitalHumanDTO> pageList(Integer page, Integer pageSize) {
Page<DigitalHuman> paged = digitalHumanService.page(Page.of(page, pageSize));
return paged.convert(DigitalHumanDTO::new);
}
@Override
public void setVoice(DigitalHumanVoiceDTO digitalHumanVoiceDTO) {
VoiceInfo voiceInfo = voiceInfoService.getById(digitalHumanVoiceDTO.getVoiceId());
Assert.notNull(voiceInfo, "语音信息不存在");
livetalkingService.setVoice(voiceInfo.getVoiceCode(), digitalHumanVoiceDTO.getDigitalHumanId());
}
@Override
public void chatCallBack(LivetalkingChatDTO digitalHumanVoiceDTO) {
// 这里可以添加处理聊天回调的逻辑
log.info("Received chat callback: {}", JSONUtil.toJsonStr(digitalHumanVoiceDTO));
DigitalHumanDialogueLog dialogueLog = new DigitalHumanDialogueLog();
dialogueLog.setId(digitalHumanVoiceDTO.getMessageId());
dialogueLog.setDigitalHumanId(digitalHumanVoiceDTO.getHumanId());
dialogueLog.setUserInput(digitalHumanVoiceDTO.getQuery());
dialogueLog.setSystemOut(digitalHumanVoiceDTO.getAnswer());
dialogueLogService.updateById(dialogueLog);
// 消息推送到弹幕系统
DanmakuMessage danmakuMessage = new DanmakuMessage();
danmakuMessage.setContent(digitalHumanVoiceDTO.getAnswer());
danmakuMessage.setRoomId(digitalHumanVoiceDTO.getRoomId());
danmakuMessage.setUserId(digitalHumanVoiceDTO.getHumanId());
danmakuMessage.setNickname("智能助手");
danmakuMessage.setType("1");
DanmakuPublisher.getInstance().publish(danmakuMessage);
}
}

@ -0,0 +1,62 @@
package com.supervision.service.impl;
import cn.hutool.core.lang.Assert;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import cn.hutool.http.HttpUtil;
import cn.hutool.json.JSONUtil;
import com.supervision.domain.LivetalkingChatDTO;
import com.supervision.service.LivetalkingService;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
@Slf4j
@Service
@RequiredArgsConstructor
public class LivetalkingServiceImpl implements LivetalkingService {
@Value("${livetalking.url}")
private String livetalkingUrl;
@Value("${livetalking.session.id:0}")
private int sessionId;
@Override
public void chat(LivetalkingChatDTO livetalkingChatDTO) {
String url = livetalkingUrl + "/human";
log.info("Sending chat request to: " + url);
Map<String, Object> param = new HashMap<>();
param.put("text", livetalkingChatDTO.getQuery());
param.put("type", "chat");
param.put("interrupt",false);
param.put("sessionid", sessionId);
// 添加额外的参数
livetalkingChatDTO.setQuery(null);
param.put("extra", livetalkingChatDTO);
HttpRequest request = HttpUtil.createPost(url)
.body(JSONUtil.toJsonStr(param))
.header("Content-Type", "application/json");
try (HttpResponse response = request.execute()) {
String body = response.body();
log.info("Chat response: {}", body);
}
}
@Override
public void setVoice(String voiceId, String digitalHumanId) {
Assert.notEmpty(voiceId, "语音ID不能为空");
String url = livetalkingUrl + "/set_voice?voiceId=" + voiceId;
HttpRequest request = HttpUtil.createGet(url);
try (HttpResponse execute = request.execute()){
String body = execute.body();
log.info("设置语音结果: {}", body);
}
}
}

@ -33,6 +33,8 @@ public class UserDetailsServiceImpl implements UserDetailsService {
if (sysUser == null) { if (sysUser == null) {
throw new UsernameNotFoundException("用户不存在: " + username); throw new UsernameNotFoundException("用户不存在: " + username);
} }
return new UserDetail(sysUser.getId(),sysUser.getUserName(), sysUser.getPassword(), authorities); UserDetail userDetail = new UserDetail(sysUser.getId(), sysUser.getUserName(), sysUser.getPassword(), authorities);
userDetail.setNickname(sysUser.getNickName());
return userDetail;
} }
} }

@ -5,6 +5,9 @@ import com.supervision.exception.UnauthorizedException;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.socket.WebSocketSession;
import java.security.Principal;
public class UserUtil { public class UserUtil {
@ -20,4 +23,13 @@ public class UserUtil {
} }
return (UserDetail) authentication.getPrincipal(); return (UserDetail) authentication.getPrincipal();
} }
public static UserDetail currentUser(WebSocketSession session){
Principal principal = session.getPrincipal();
if (principal instanceof UserDetail userDetail) {
return userDetail;
} else {
throw new UnauthorizedException("未登录或登录已过期,请重新登录");
}
}
} }

@ -39,6 +39,10 @@ mybatis-plus:
mapper-locations: classpath*:mapper/*.xml mapper-locations: classpath*:mapper/*.xml
configuration: configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
livetalking:
url: http://192.168.10.96:8010
session:
id: 0
jwt: jwt:
secret: "DlHaPUePiN6MyvpMpsMq/t6swzMHqtrRFd2YnofKz4k=" # JWT密钥 使用官方推荐方式生成 Base64.getEncoder().encodeToString(Keys.secretKeyFor(SignatureAlgorithm.HS256).getEncoded()); secret: "DlHaPUePiN6MyvpMpsMq/t6swzMHqtrRFd2YnofKz4k=" # JWT密钥 使用官方推荐方式生成 Base64.getEncoder().encodeToString(Keys.secretKeyFor(SignatureAlgorithm.HS256).getEncoded());
expiration: 86400000 # 1小时3600000 1天86400000 expiration: 86400000 # 1小时3600000 1天86400000

@ -0,0 +1,146 @@
class DanmakuClient {
constructor(container) {
this.container = container;
this.socket = null;
this.roomId = null;
this.tracks = [];
this.initTracks();
}
initTracks() {
const height = this.container.clientHeight;
const trackHeight = height / 5;
for (let i = 0; i < 5; i++) {
this.tracks.push({
top: i * trackHeight,
inUse: false
});
}
}
connect(wsUrl, roomId) {
if (this.socket) this.disconnect();
this.roomId = roomId;
this.socket = new WebSocket(`${wsUrl}?roomId=${roomId}`);
this.socket.onopen = () => {
console.log('WebSocket connected');
document.getElementById('connect').disabled = true;
document.getElementById('disconnect').disabled = false;
};
this.socket.onclose = () => {
console.log('WebSocket disconnected');
document.getElementById('connect').disabled = false;
document.getElementById('disconnect').disabled = true;
};
this.socket.onmessage = (event) => {
const danmaku = JSON.parse(event.data);
this.displayDanmaku(danmaku);
};
this.socket.onerror = (error) => {
console.error('WebSocket error:', error);
};
}
disconnect() {
if (this.socket) {
this.socket.close();
this.socket = null;
}
}
sendDanmaku(content, user, style) {
if (!this.socket || this.socket.readyState !== WebSocket.OPEN) {
alert('请先连接到弹幕服务器');
return;
}
if (!content || content.trim() === '') {
alert('弹幕内容不能为空');
return;
}
const message = {
roomId: this.roomId,
content,
userId: user || 'anonymous',
color: style?.color || '#ffffff',
size: style?.size || 24
};
this.socket.send(JSON.stringify(message));
}
displayDanmaku(danmaku) {
const danmakuElement = document.createElement('div');
danmakuElement.className = 'danmaku';
danmakuElement.textContent = danmaku.content;
danmakuElement.style.color = danmaku.color || '#ffffff';
danmakuElement.style.fontSize = `${danmaku.size || 24}px`;
const track = this.findAvailableTrack();
if (!track) return;
danmakuElement.style.top = `${track.top}px`;
this.container.appendChild(danmakuElement);
const startX = this.container.clientWidth;
const endX = -danmakuElement.clientWidth;
let startTime = null;
const animate = (timestamp) => {
if (!startTime) startTime = timestamp;
const progress = (timestamp - startTime) / 10000;
if (progress < 1) {
const x = startX + (endX - startX) * progress;
danmakuElement.style.transform = `translateX(${x}px)`;
requestAnimationFrame(animate);
} else {
this.container.removeChild(danmakuElement);
track.inUse = false;
}
};
track.inUse = true;
requestAnimationFrame(animate);
}
findAvailableTrack() {
return this.tracks.find(track => !track.inUse);
}
}
// 初始化客户端
const container = document.getElementById('danmaku-container');
const client = new DanmakuClient(container);
// 绑定事件
document.getElementById('connect').addEventListener('click', () => {
const roomId = document.getElementById('roomId').value;
client.connect('ws://' + window.location.host + '/ai-platform/ws/danmaku', roomId);
});
document.getElementById('disconnect').addEventListener('click', () => {
client.disconnect();
});
document.getElementById('send').addEventListener('click', () => {
const content = document.getElementById('message').value;
const color = document.getElementById('color').value;
const size = document.getElementById('size').value;
client.sendDanmaku(content, null, { color, size });
document.getElementById('message').value = '';
});
document.getElementById('message').addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
document.getElementById('send').click();
}
});

@ -0,0 +1,51 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>弹幕系统 - Spring Boot</title>
<style>
#danmaku-container {
width: 800px;
height: 500px;
background: #000;
position: relative;
overflow: hidden;
}
.danmaku {
position: absolute;
white-space: nowrap;
color: #fff;
font-size: 16px;
text-shadow: 1px 1px 2px #000;
will-change: transform;
}
</style>
</head>
<body>
<h1>弹幕系统</h1>
<div>
<label>房间ID: <input id="roomId" value="room1"></label>
<button id="connect">连接</button>
<button id="disconnect" disabled>断开</button>
</div>
<div id="danmaku-container"></div>
<div>
<input id="message" placeholder="输入弹幕内容">
<button id="send">发送</button>
<div>
<label>颜色: <input type="color" id="color" value="#ffffff"></label>
<label>大小:
<select id="size">
<option value="16"></option>
<option value="24" selected></option>
<option value="32"></option>
</select>
</label>
</div>
</div>
<script src="danmaku.js"></script>
</body>
</html>

@ -1,5 +1,7 @@
package com.supervision; package com.supervision;
import com.hankcs.hanlp.seg.common.Term;
import com.hankcs.hanlp.tokenizer.StandardTokenizer;
import com.supervision.domain.SysByteArray; import com.supervision.domain.SysByteArray;
import com.supervision.service.SysByteArrayService; import com.supervision.service.SysByteArrayService;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@ -26,4 +28,32 @@ public class PlatformApplicationTest {
SysByteArray sysByteArray = sysByteArrayService.getById("1945676008645058562"); SysByteArray sysByteArray = sysByteArrayService.getById("1945676008645058562");
log.info("查询结果: {}", sysByteArray); log.info("查询结果: {}", sysByteArray);
} }
@Test
void isQuestionTest() {
String sentence = "你是谁";
boolean isQuestion = isQuestion(sentence);
System.out.println("是否是问句: " + isQuestion);
}
private boolean isQuestion(String sentence) {
String[] questionWords = {"什么", "为什么", "如何", "哪", "谁", "多少", "是否", "能否", "是不是"};
String[] questionEndings = {"吗", "呢", "", "?"};
// 结尾判断
for (String end : questionEndings) {
if (sentence.trim().endsWith(end)) {
return true;
}
}
// 分词判断
for (Term term : StandardTokenizer.segment(sentence)) {
for (String qw : questionWords) {
if (term.word.equals(qw)) {
return true;
}
}
}
return false;
}
} }

Loading…
Cancel
Save