diff --git a/src/main/java/com/yolo/keyborad/common/ErrorCode.java b/src/main/java/com/yolo/keyborad/common/ErrorCode.java index f0943b6..f2914e4 100644 --- a/src/main/java/com/yolo/keyborad/common/ErrorCode.java +++ b/src/main/java/com/yolo/keyborad/common/ErrorCode.java @@ -23,6 +23,11 @@ public enum ErrorCode { FILE_NAME_ERROR(40002, "文件名错误"), FILE_TYPE_ERROR(40004, "文件类型不支持,仅支持图片格式"), FILE_SIZE_EXCEED(40005, "文件大小超出限制,最大支持5MB"), + CHAT_MESSAGE_EMPTY(40006, "聊天消息不能为空"), + CHAT_CHARACTER_ID_EMPTY(40007, "键盘人设ID不能为空"), + CHAT_CHARACTER_NOT_FOUND(40008, "键盘人设不存在"), + CHAT_MESSAGE_TOO_LONG(40009, "聊天消息过长,最大支持1000字符"), + CHAT_SAVE_DATA_EMPTY(40010, "保存数据不能为空"), TOKEN_NOT_FOUND(40102, "未能读取到有效用户令牌"), TOKEN_INVALID(40103, "令牌无效"), TOKEN_TIMEOUT(40104, "令牌已过期"), diff --git a/src/main/java/com/yolo/keyborad/controller/ChatController.java b/src/main/java/com/yolo/keyborad/controller/ChatController.java index 3f90ab5..f3bd5db 100644 --- a/src/main/java/com/yolo/keyborad/controller/ChatController.java +++ b/src/main/java/com/yolo/keyborad/controller/ChatController.java @@ -2,8 +2,11 @@ package com.yolo.keyborad.controller; import cn.dev33.satoken.stp.StpUtil; import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.StrUtil; import com.yolo.keyborad.common.BaseResponse; +import com.yolo.keyborad.common.ErrorCode; import com.yolo.keyborad.common.ResultUtils; +import com.yolo.keyborad.exception.BusinessException; import com.yolo.keyborad.mapper.QdrantPayloadMapper; import com.yolo.keyborad.model.dto.chat.ChatReq; import com.yolo.keyborad.model.dto.chat.ChatSaveReq; @@ -13,7 +16,6 @@ import com.yolo.keyborad.service.KeyboardCharacterService; import com.yolo.keyborad.service.impl.QdrantVectorService; import io.qdrant.client.grpc.JsonWithInt; import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; @@ -37,10 +39,12 @@ import java.util.Map; @RestController @RequestMapping("/chat") @Slf4j -@CrossOrigin @Tag(name = "聊天", description = "聊天接口") public class ChatController { + // 最大消息长度限制 + private static final int MAX_MESSAGE_LENGTH = 1000; + @Resource private ChatClient client; @@ -57,8 +61,35 @@ public class ChatController { @PostMapping("/talk") @Operation(summary = "聊天润色接口", description = "聊天润色接口") public Flux> talk(@RequestBody ChatReq chatReq){ + // 1. 参数校验 + if (chatReq == null) { + log.error("聊天请求参数为空"); + throw new BusinessException(ErrorCode.PARAMS_ERROR); + } + + if (chatReq.getCharacterId() == null) { + log.error("键盘人设ID为空"); + throw new BusinessException(ErrorCode.CHAT_CHARACTER_ID_EMPTY); + } + + if (StrUtil.isBlank(chatReq.getMessage())) { + log.error("聊天消息为空"); + throw new BusinessException(ErrorCode.CHAT_MESSAGE_EMPTY); + } + + if (chatReq.getMessage().length() > MAX_MESSAGE_LENGTH) { + log.error("聊天消息过长,长度: {}", chatReq.getMessage().length()); + throw new BusinessException(ErrorCode.CHAT_MESSAGE_TOO_LONG); + } + + // 2. 验证键盘人设是否存在 KeyboardCharacter character = keyboardCharacterService.getById(chatReq.getCharacterId()); - // 1. LLM 流式输出 + if (character == null) { + log.error("键盘人设不存在,ID: {}", chatReq.getCharacterId()); + throw new BusinessException(ErrorCode.CHAT_CHARACTER_NOT_FOUND); + } + + // 3. LLM 流式输出 Flux llmFlux = client .prompt(character.getPrompt()) .system(""" @@ -79,12 +110,12 @@ public class ChatController { .mapToObj(cp -> new String(Character.toChars(cp))) .toList(); - // 你可以在这里按 3~5 个字符再拼一拼 + // 按 3 个字符批量发送 List batched = new ArrayList<>(); StringBuilder sb = new StringBuilder(); for (String ch : chars) { sb.append(ch); - if (sb.length() >= 3) { // 这里的 3 可以自己调 + if (sb.length() >= 3) { batched.add(sb.toString()); sb.setLength(0); } @@ -95,25 +126,33 @@ public class ChatController { return Flux.fromIterable(batched) .map(s -> new ChatStreamMessage("llm_chunk", s)); - }); -// .map(chunk -> new ChatStreamMessage("llm_chunk", chunk)); + }) + .doOnError(error -> log.error("LLM调用失败", error)) + .onErrorResume(error -> + Flux.just(new ChatStreamMessage("error", "LLM服务暂时不可用,请稍后重试")) + ); - // 2. 向量搜索Flux(一次性发送搜索结果) + // 4. 向量搜索Flux(一次性发送搜索结果) Flux searchFlux = Mono .fromCallable(() -> qdrantVectorService.searchText(chatReq.getMessage())) .subscribeOn(Schedulers.boundedElastic()) // 避免阻塞 event-loop .map(list -> new ChatStreamMessage("search_result", list)) + .doOnError(error -> log.error("向量搜索失败", error)) + .onErrorResume(error -> + Mono.just(new ChatStreamMessage("search_result", new ArrayList<>())) + ) .flux(); - // 3. 结束标记 + + // 5. 结束标记 Flux doneFlux = Flux.just(new ChatStreamMessage("done", null)); - // 4. 合并所有Flux + // 6. 合并所有Flux Flux merged = Flux.merge(llmFlux, searchFlux) .concatWith(doneFlux); - // 5. SSE 包装 + // 7. SSE 包装 return merged.map(msg -> ServerSentEvent.builder(msg) .event(msg.getType()) @@ -123,13 +162,30 @@ public class ChatController { @PostMapping("/save_embed") - @Operation(summary = "保存润色后的句子", description = "保存润色后的句子") - @Parameter(name = "userInput",required = true,description = "测试聊天接口",example = "talk to something") - public BaseResponse testTalkWithVector(@RequestBody ChatSaveReq chatSaveReq) { + @Operation(summary = "保存润色后的句子", description = "保存用户选择的润色句子到向量数据库") + public BaseResponse saveChatEmbed(@RequestBody ChatSaveReq chatSaveReq) { + // 1. 参数校验 + if (chatSaveReq == null) { + log.error("保存请求参数为空"); + throw new BusinessException(ErrorCode.PARAMS_ERROR); + } + + if (StrUtil.isBlank(chatSaveReq.getUserText())) { + log.error("用户原始文本为空"); + throw new BusinessException(ErrorCode.CHAT_SAVE_DATA_EMPTY); + } + + // 2. 生成嵌入向量 float[] embed = embeddingModel.embed(chatSaveReq.getUserText()); + + // 3. 设置用户ID chatSaveReq.setUserId(StpUtil.getLoginIdAsLong()); + + // 4. 转换为Qdrant payload并保存 Map map = QdrantPayloadMapper.toQdrantPayload(chatSaveReq); qdrantVectorService.upsertPoint(IdUtil.getSnowflakeNextId(), embed, map); + + log.info("聊天嵌入保存成功,用户ID: {}, 文本长度: {}", chatSaveReq.getUserId(), chatSaveReq.getUserText().length()); return ResultUtils.success(true); } }