feat(ai-companion): 新增AI伴侣模块及白名单路径
This commit is contained in:
@@ -28,6 +28,8 @@ public enum ErrorCode {
|
|||||||
CHAT_CHARACTER_NOT_FOUND(40008, "键盘人设不存在"),
|
CHAT_CHARACTER_NOT_FOUND(40008, "键盘人设不存在"),
|
||||||
CHAT_MESSAGE_TOO_LONG(40009, "聊天消息过长,最大支持1000字符"),
|
CHAT_MESSAGE_TOO_LONG(40009, "聊天消息过长,最大支持1000字符"),
|
||||||
CHAT_SAVE_DATA_EMPTY(40010, "保存数据不能为空"),
|
CHAT_SAVE_DATA_EMPTY(40010, "保存数据不能为空"),
|
||||||
|
COMPANION_MESSAGE_EMPTY(40011, "消息内容不能为空"),
|
||||||
|
COMPANION_ID_EMPTY(40012, "AI陪聊角色ID不能为空"),
|
||||||
TOKEN_NOT_FOUND(40102, "未能读取到有效用户令牌"),
|
TOKEN_NOT_FOUND(40102, "未能读取到有效用户令牌"),
|
||||||
TOKEN_INVALID(40103, "令牌无效"),
|
TOKEN_INVALID(40103, "令牌无效"),
|
||||||
TOKEN_TIMEOUT(40104, "令牌已过期"),
|
TOKEN_TIMEOUT(40104, "令牌已过期"),
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import com.baomidou.mybatisplus.core.metadata.IPage;
|
|||||||
import com.yolo.keyborad.common.BaseResponse;
|
import com.yolo.keyborad.common.BaseResponse;
|
||||||
import com.yolo.keyborad.common.ResultUtils;
|
import com.yolo.keyborad.common.ResultUtils;
|
||||||
import com.yolo.keyborad.model.dto.PageDTO;
|
import com.yolo.keyborad.model.dto.PageDTO;
|
||||||
import com.yolo.keyborad.model.entity.KeyboardAiCompanion;
|
import com.yolo.keyborad.model.vo.AiCompanionVO;
|
||||||
import com.yolo.keyborad.service.KeyboardAiCompanionService;
|
import com.yolo.keyborad.service.KeyboardAiCompanionService;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
@@ -27,8 +27,8 @@ public class AiCompanionController {
|
|||||||
|
|
||||||
@PostMapping("/page")
|
@PostMapping("/page")
|
||||||
@Operation(summary = "分页查询AI陪聊角色", description = "分页查询已上线的AI陪聊角色列表")
|
@Operation(summary = "分页查询AI陪聊角色", description = "分页查询已上线的AI陪聊角色列表")
|
||||||
public BaseResponse<IPage<KeyboardAiCompanion>> pageList(@RequestBody PageDTO pageDTO) {
|
public BaseResponse<IPage<AiCompanionVO>> pageList(@RequestBody PageDTO pageDTO) {
|
||||||
IPage<KeyboardAiCompanion> result = aiCompanionService.pageList(pageDTO.getPageNum(), pageDTO.getPageSize());
|
IPage<AiCompanionVO> result = aiCompanionService.pageList(pageDTO.getPageNum(), pageDTO.getPageSize());
|
||||||
return ResultUtils.success(result);
|
return ResultUtils.success(result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import com.yolo.keyborad.common.ErrorCode;
|
|||||||
import com.yolo.keyborad.common.ResultUtils;
|
import com.yolo.keyborad.common.ResultUtils;
|
||||||
import com.yolo.keyborad.exception.BusinessException;
|
import com.yolo.keyborad.exception.BusinessException;
|
||||||
import com.yolo.keyborad.mapper.QdrantPayloadMapper;
|
import com.yolo.keyborad.mapper.QdrantPayloadMapper;
|
||||||
|
import com.yolo.keyborad.model.dto.chat.ChatMessageReq;
|
||||||
import com.yolo.keyborad.model.dto.chat.ChatReq;
|
import com.yolo.keyborad.model.dto.chat.ChatReq;
|
||||||
import com.yolo.keyborad.model.dto.chat.ChatSaveReq;
|
import com.yolo.keyborad.model.dto.chat.ChatSaveReq;
|
||||||
import com.yolo.keyborad.model.dto.chat.ChatStreamMessage;
|
import com.yolo.keyborad.model.dto.chat.ChatStreamMessage;
|
||||||
@@ -50,13 +51,16 @@ public class ChatController {
|
|||||||
|
|
||||||
@PostMapping("/message")
|
@PostMapping("/message")
|
||||||
@Operation(summary = "同步对话", description = "发送消息给大模型,同步返回 AI 响应,异步生成音频")
|
@Operation(summary = "同步对话", description = "发送消息给大模型,同步返回 AI 响应,异步生成音频")
|
||||||
public BaseResponse<ChatMessageVO> message(@RequestParam("content") String content) {
|
public BaseResponse<ChatMessageVO> message(@RequestBody ChatMessageReq req ) {
|
||||||
if (StrUtil.isBlank(content)) {
|
if (StrUtil.isBlank(req.getContent())) {
|
||||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "消息内容不能为空");
|
throw new BusinessException(ErrorCode.COMPANION_MESSAGE_EMPTY);
|
||||||
|
}
|
||||||
|
if (req.getCompanionId() == null) {
|
||||||
|
throw new BusinessException(ErrorCode.COMPANION_ID_EMPTY);
|
||||||
}
|
}
|
||||||
|
|
||||||
String userId = StpUtil.getLoginIdAsString();
|
String userId = StpUtil.getLoginIdAsString();
|
||||||
ChatMessageVO result = chatService.message(content, userId);
|
ChatMessageVO result = chatService.message(req.getContent(), userId, req.getCompanionId());
|
||||||
|
|
||||||
return ResultUtils.success(result);
|
return ResultUtils.success(result);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package com.yolo.keyborad.model.dto.chat;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* @author: ziin
|
||||||
|
* @date: 2025/12/8 15:05
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Schema(description = "同步对话请求")
|
||||||
|
public class ChatMessageReq {
|
||||||
|
|
||||||
|
@Schema(description = "消息内容", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
private String content;
|
||||||
|
|
||||||
|
@Schema(description = "AI陪聊角色ID", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
private Long companionId;
|
||||||
|
}
|
||||||
@@ -138,4 +138,12 @@ public class KeyboardAiCompanion {
|
|||||||
@TableField(value = "updated_at")
|
@TableField(value = "updated_at")
|
||||||
@Schema(description="更新时间")
|
@Schema(description="更新时间")
|
||||||
private Date updatedAt;
|
private Date updatedAt;
|
||||||
|
|
||||||
|
@TableField(value = "prologue")
|
||||||
|
@Schema(description="开场白")
|
||||||
|
private String prologue;
|
||||||
|
|
||||||
|
@TableField(value = "prologue_audio")
|
||||||
|
@Schema(description="开场白音频")
|
||||||
|
private String prologueAudio;
|
||||||
}
|
}
|
||||||
60
src/main/java/com/yolo/keyborad/model/vo/AiCompanionVO.java
Normal file
60
src/main/java/com/yolo/keyborad/model/vo/AiCompanionVO.java
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
package com.yolo.keyborad.model.vo;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* @author: ziin
|
||||||
|
* @date: 2026/1/26
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Schema(description = "AI陪聊角色VO")
|
||||||
|
public class AiCompanionVO {
|
||||||
|
|
||||||
|
@Schema(description = "陪聊角色唯一ID")
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Schema(description = "角色名称")
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
@Schema(description = "角色头像URL")
|
||||||
|
private String avatarUrl;
|
||||||
|
|
||||||
|
@Schema(description = "角色封面图URL")
|
||||||
|
private String coverImageUrl;
|
||||||
|
|
||||||
|
@Schema(description = "角色性别(male / female / other)")
|
||||||
|
private String gender;
|
||||||
|
|
||||||
|
@Schema(description = "角色年龄段描述")
|
||||||
|
private String ageRange;
|
||||||
|
|
||||||
|
@Schema(description = "一句话人设描述")
|
||||||
|
private String shortDesc;
|
||||||
|
|
||||||
|
@Schema(description = "角色详细介绍文案")
|
||||||
|
private String introText;
|
||||||
|
|
||||||
|
@Schema(description = "角色性格标签数组")
|
||||||
|
private String personalityTags;
|
||||||
|
|
||||||
|
@Schema(description = "角色说话风格")
|
||||||
|
private String speakingStyle;
|
||||||
|
|
||||||
|
@Schema(description = "排序权重")
|
||||||
|
private Integer sortOrder;
|
||||||
|
|
||||||
|
@Schema(description = "角色热度评分")
|
||||||
|
private Integer popularityScore;
|
||||||
|
|
||||||
|
@Schema(description = "开场白")
|
||||||
|
private String prologue;
|
||||||
|
|
||||||
|
@Schema(description = "开场白音频")
|
||||||
|
private String prologueAudio;
|
||||||
|
|
||||||
|
@Schema(description = "创建时间")
|
||||||
|
private Date createdAt;
|
||||||
|
}
|
||||||
@@ -20,9 +20,10 @@ public interface ChatService {
|
|||||||
*
|
*
|
||||||
* @param content 用户消息内容
|
* @param content 用户消息内容
|
||||||
* @param userId 用户ID
|
* @param userId 用户ID
|
||||||
|
* @param companionId AI陪聊角色ID
|
||||||
* @return AI 响应 + 音频任务 ID
|
* @return AI 响应 + 音频任务 ID
|
||||||
*/
|
*/
|
||||||
ChatMessageVO message(String content, String userId);
|
ChatMessageVO message(String content, String userId, Long companionId);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 查询音频任务状态
|
* 查询音频任务状态
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.yolo.keyborad.service;
|
|||||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||||
import com.yolo.keyborad.model.entity.KeyboardAiCompanion;
|
import com.yolo.keyborad.model.entity.KeyboardAiCompanion;
|
||||||
import com.baomidou.mybatisplus.extension.service.IService;
|
import com.baomidou.mybatisplus.extension.service.IService;
|
||||||
|
import com.yolo.keyborad.model.vo.AiCompanionVO;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* @author: ziin
|
* @author: ziin
|
||||||
@@ -17,5 +18,13 @@ public interface KeyboardAiCompanionService extends IService<KeyboardAiCompanion
|
|||||||
* @param pageSize 每页数量
|
* @param pageSize 每页数量
|
||||||
* @return 分页结果
|
* @return 分页结果
|
||||||
*/
|
*/
|
||||||
IPage<KeyboardAiCompanion> pageList(Integer pageNum, Integer pageSize);
|
IPage<AiCompanionVO> pageList(Integer pageNum, Integer pageSize);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据AI人设ID获取系统提示词
|
||||||
|
*
|
||||||
|
* @param companionId AI人设ID
|
||||||
|
* @return 系统提示词
|
||||||
|
*/
|
||||||
|
String getSystemPromptById(Long companionId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,6 +74,9 @@ public class ChatServiceImpl implements ChatService {
|
|||||||
@Resource
|
@Resource
|
||||||
private UserService userService;
|
private UserService userService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private KeyboardAiCompanionService aiCompanionService;
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
private ElevenLabsService elevenLabsService;
|
private ElevenLabsService elevenLabsService;
|
||||||
|
|
||||||
@@ -352,15 +355,19 @@ public class ChatServiceImpl implements ChatService {
|
|||||||
*
|
*
|
||||||
* @param content 用户消息内容
|
* @param content 用户消息内容
|
||||||
* @param userId 用户ID
|
* @param userId 用户ID
|
||||||
|
* @param companionId AI陪聊角色ID
|
||||||
* @return AI 响应 + 音频任务 ID
|
* @return AI 响应 + 音频任务 ID
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public ChatMessageVO message(String content, String userId) {
|
public ChatMessageVO message(String content, String userId, Long companionId) {
|
||||||
log.info("同步对话请求, userId: {}, content: {}", userId, content);
|
log.info("同步对话请求, userId: {}, companionId: {}, content: {}", userId, companionId, content);
|
||||||
|
|
||||||
long startTime = System.currentTimeMillis();
|
long startTime = System.currentTimeMillis();
|
||||||
|
|
||||||
String response = callLLM(content);
|
// 获取AI人设的系统提示词
|
||||||
|
String systemPrompt = aiCompanionService.getSystemPromptById(companionId);
|
||||||
|
|
||||||
|
String response = callLLM(content, systemPrompt);
|
||||||
|
|
||||||
long llmDuration = System.currentTimeMillis() - startTime;
|
long llmDuration = System.currentTimeMillis() - startTime;
|
||||||
log.info("LLM 完成, userId: {}, 耗时: {}ms, 响应长度: {}", userId, llmDuration, response.length());
|
log.info("LLM 完成, userId: {}, 耗时: {}ms, 响应长度: {}", userId, llmDuration, response.length());
|
||||||
@@ -384,19 +391,7 @@ public class ChatServiceImpl implements ChatService {
|
|||||||
/**
|
/**
|
||||||
* 调用 LLM 生成响应
|
* 调用 LLM 生成响应
|
||||||
*/
|
*/
|
||||||
private String callLLM(String content) {
|
private String callLLM(String content, String systemPrompt) {
|
||||||
String systemPrompt = """
|
|
||||||
ROLE: You are "Tiffany", a 17-year-old popular high school student from Beverly Hills.
|
|
||||||
You are sassy, judgmental, impatient, but secretly caring.
|
|
||||||
|
|
||||||
TASK: Generate dialogue scripts optimized for ElevenLabs TTS.
|
|
||||||
Use emotional cues in brackets like [sighs], [giggles], [scoffs], [whispers], [yells].
|
|
||||||
Use punctuation for tone: ... for hesitation, ! for energy.
|
|
||||||
Use fillers naturally: "like", "literally", "omg".
|
|
||||||
|
|
||||||
Keep responses concise (2-3 sentences max) for real-time conversation.
|
|
||||||
""";
|
|
||||||
|
|
||||||
return client
|
return client
|
||||||
.prompt()
|
.prompt()
|
||||||
.system(systemPrompt)
|
.system(systemPrompt)
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
package com.yolo.keyborad.service.impl;
|
package com.yolo.keyborad.service.impl;
|
||||||
|
|
||||||
|
import cn.hutool.core.bean.BeanUtil;
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
|
import com.yolo.keyborad.common.ErrorCode;
|
||||||
|
import com.yolo.keyborad.exception.BusinessException;
|
||||||
|
import com.yolo.keyborad.model.vo.AiCompanionVO;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
import com.yolo.keyborad.model.entity.KeyboardAiCompanion;
|
import com.yolo.keyborad.model.entity.KeyboardAiCompanion;
|
||||||
@@ -17,13 +21,26 @@ import com.yolo.keyborad.service.KeyboardAiCompanionService;
|
|||||||
public class KeyboardAiCompanionServiceImpl extends ServiceImpl<KeyboardAiCompanionMapper, KeyboardAiCompanion> implements KeyboardAiCompanionService {
|
public class KeyboardAiCompanionServiceImpl extends ServiceImpl<KeyboardAiCompanionMapper, KeyboardAiCompanion> implements KeyboardAiCompanionService {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public IPage<KeyboardAiCompanion> pageList(Integer pageNum, Integer pageSize) {
|
public IPage<AiCompanionVO> pageList(Integer pageNum, Integer pageSize) {
|
||||||
Page<KeyboardAiCompanion> page = new Page<>(pageNum, pageSize);
|
Page<KeyboardAiCompanion> page = new Page<>(pageNum, pageSize);
|
||||||
LambdaQueryWrapper<KeyboardAiCompanion> queryWrapper = new LambdaQueryWrapper<>();
|
LambdaQueryWrapper<KeyboardAiCompanion> queryWrapper = new LambdaQueryWrapper<>();
|
||||||
queryWrapper.eq(KeyboardAiCompanion::getStatus, 1)
|
queryWrapper.eq(KeyboardAiCompanion::getStatus, 1)
|
||||||
.eq(KeyboardAiCompanion::getVisibility, 1)
|
.eq(KeyboardAiCompanion::getVisibility, 1)
|
||||||
.orderByDesc(KeyboardAiCompanion::getSortOrder)
|
.orderByDesc(KeyboardAiCompanion::getSortOrder)
|
||||||
.orderByDesc(KeyboardAiCompanion::getPopularityScore);
|
.orderByDesc(KeyboardAiCompanion::getPopularityScore);
|
||||||
return this.page(page, queryWrapper);
|
IPage<KeyboardAiCompanion> entityPage = this.page(page, queryWrapper);
|
||||||
|
return entityPage.convert(entity -> BeanUtil.copyProperties(entity, AiCompanionVO.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getSystemPromptById(Long companionId) {
|
||||||
|
KeyboardAiCompanion companion = this.getById(companionId);
|
||||||
|
if (companion == null) {
|
||||||
|
throw new BusinessException(ErrorCode.NOT_FOUND_ERROR, "AI陪聊角色不存在");
|
||||||
|
}
|
||||||
|
if (companion.getStatus() != 1) {
|
||||||
|
throw new BusinessException(ErrorCode.PARAMS_ERROR, "AI陪聊角色已下线");
|
||||||
|
}
|
||||||
|
return companion.getSystemPrompt();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user