From 966dbe6a81868c845c96a3d18904f0f844ed25f5 Mon Sep 17 00:00:00 2001 From: wangxiangshun Date: Thu, 6 Nov 2025 20:14:01 +0800 Subject: [PATCH] =?UTF-8?q?=E5=BC=82=E5=B8=B8=E4=BF=A1=E6=81=AF=E5=A4=84?= =?UTF-8?q?=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../vetti/socket/ChatWebSocketHandler.java | 314 +++++++++--------- .../src/main/resources/application-druid.yml | 6 +- .../main/resources/i18n/messages.properties | 4 +- .../target/classes/application-druid.yml | 6 +- .../web/exception/GlobalExceptionHandler.java | 3 + 5 files changed, 162 insertions(+), 171 deletions(-) diff --git a/vetti-admin/src/main/java/com/vetti/socket/ChatWebSocketHandler.java b/vetti-admin/src/main/java/com/vetti/socket/ChatWebSocketHandler.java index 244bb87..fc4e322 100644 --- a/vetti-admin/src/main/java/com/vetti/socket/ChatWebSocketHandler.java +++ b/vetti-admin/src/main/java/com/vetti/socket/ChatWebSocketHandler.java @@ -12,7 +12,6 @@ import com.vetti.common.config.RuoYiConfig; import com.vetti.common.utils.spring.SpringUtils; import com.vetti.hotake.domain.HotakeProblemBaseInfo; import com.vetti.hotake.service.IHotakeProblemBaseInfoService; -import io.swagger.models.auth.In; import lombok.extern.slf4j.Slf4j; import org.apache.commons.io.FileUtils; import org.springframework.stereotype.Component; @@ -177,142 +176,76 @@ public class ChatWebSocketHandler { //把提问的文字发送给CPT(流式处理) OpenAiStreamClient aiStreamClient = SpringUtils.getBean(OpenAiStreamClient.class); log.info("AI提示词为:{}", promptJson); - //Score: 返回的是评分,后面只会跟着一个 - //FOLLOW-UP: 返回的是问题,后面的每一行都是问题 - aiStreamClient.streamChat(promptJson, new OpenAiStreamListenerService() { - String isScore = "0"; - //是否结束面试 - Boolean flag = true; - String resultText = ""; - String resultEvaluate = ""; - //是否遇到问题记录 - Boolean isFlow = false; - @Override - public void onMessage(String content) { - log.info("返回AI结果:{}", content.replaceAll("\n", "")); - if (StrUtil.isEmpty(resultText)) { - resultText = content; - } else { - resultText = resultText + content; - } - String contentData = content.replaceAll("\n", ""); - //记录获取的分数,并且验证分数是否完成 - //获取评分 - if (contentData.contains(SCORE_FLAG)) { - isScore = "1"; - } - if ("1".equals(isScore)) { - //获取的是评分,并且记录评分 - flag = handleScoreRecord(content, session); - } - if (contentData.contains(QUESTION_FLAG)) { - isFlow = true; - } - if (flag) { + //先获取回答的评分,是否符合要求 + Boolean isEndFlag = getInterviewScore(promptJson, session, ""); + if(isEndFlag){ + log.info("面试回答符合条件规则,继续追问啦!!!!!"); + aiStreamClient.streamChat(promptJson, new OpenAiStreamListenerService() { + int resultNum = (int) (Math.random() * 2) + 1; + @Override + public void onMessage(String content) { + log.info("返回AI结果:{}", content.replaceAll("\n", "")); + //获取1和2的随机数 + if(resultNum == 1){ + content = ""; + } + resultNum = resultNum+1; + String contentData = content.replaceAll("\n", ""); //返回是追问的问题 - if (contentData.contains(QUESTION_FLAG)) { - //获取的是追问的问题 - contentData = contentData.replace(QUESTION_FLAG, ""); - if (StrUtil.isNotEmpty(contentData)) { - //对问题进行数据缓存 - cacheQuestionResult.put(session.getId(), contentData); - //开始进行语音输出-流式持续输出 - sendTTSBuffer(clientId, contentData, session); - // 实时输出内容 - try { - //把文本也给前端返回去 - Map dataText = new HashMap<>(); - dataText.put("type", "question"); - dataText.put("content", contentData); - session.getBasicRemote().sendText(JSONUtil.toJsonStr(dataText)); - } catch (Exception e) { - e.printStackTrace(); + //获取的是追问的问题 + if (StrUtil.isNotEmpty(contentData)) { + //对问题进行数据缓存 + cacheQuestionResult.put(session.getId(), contentData); + //开始进行语音输出-流式持续输出 + sendTTSBuffer(clientId, contentData, session); + // 实时输出内容 + try { + //把文本也给前端返回去 + Map dataText = new HashMap<>(); + dataText.put("type", "question"); + dataText.put("content", contentData); + session.getBasicRemote().sendText(JSONUtil.toJsonStr(dataText)); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + @Override + public void onComplete() { + try { + //开始往缓存中记录提问的问题 + String questionResult = cacheQuestionResult.get(session.getId()); + if (StrUtil.isNotEmpty(questionResult)) { + //获取缓存记录 + String msgMapData = cacheMsgMapData.get(session.getId()); + if (StrUtil.isNotEmpty(msgMapData)) { + List list = JSONUtil.toList(msgMapData, Map.class); + Map mapEntity = new HashMap<>(); + mapEntity.put("role", "user"); + mapEntity.put("content", "Question:" + questionResult + "\\nCandidate Answer:{}"); + list.add(mapEntity); + cacheMsgMapData.put(session.getId(), JSONUtil.toJsonStr(list)); } + } + //清空问题 + cacheQuestionResult.put(session.getId(), ""); - } - } - if(!isFlow){ - if (StrUtil.isEmpty(resultEvaluate)) { - resultEvaluate = content; - } else { - resultEvaluate = resultEvaluate + content; - } + } catch (Exception e) { + throw new RuntimeException(e); } } - } - @Override - public void onComplete() { - try { - //开始往缓存中记录提问的问题 - String questionResult = cacheQuestionResult.get(session.getId()); - if(StrUtil.isNotEmpty(questionResult)){ - //获取缓存记录 - String msgMapData = cacheMsgMapData.get(session.getId()); - if (StrUtil.isNotEmpty(msgMapData)) { - List list = JSONUtil.toList(msgMapData, Map.class); - Map mapEntity = new HashMap<>(); - mapEntity.put("role", "user"); - mapEntity.put("content", "Question:" + questionResult + "\\nCandidate Answer:{}"); - list.add(mapEntity); - cacheMsgMapData.put(session.getId(), JSONUtil.toJsonStr(list)); - } - } - //清空问题 - cacheQuestionResult.put(session.getId(), ""); - if(!flag || !isFlow){ - //理解结束面试 - //发送面试结束的通知已经评分 - handleInterviewEnd(session, resultEvaluate); - } - } catch (Exception e) { - throw new RuntimeException(e); + @Override + public void onError(Throwable throwable) { + throwable.printStackTrace(); } - } - - @Override - public void onError(Throwable throwable) { - throwable.printStackTrace(); - } - }); + }); + } } } else if ("end".equals(resultFlag)) { - //暂时的业务逻辑 - //发送面试官结束语音流 - String openingPathUrl = RuoYiConfig.getProfile() + VOICE_SYSTEM_DIR + "end.wav"; - try { - //文件转换成文件流 - ByteBuffer outByteBuffer = convertFileToByteBuffer(openingPathUrl); - //发送文件流数据 - session.getBasicRemote().sendBinary(outByteBuffer); - // 发送响应确认 - log.info("结束返回面试官语音信息:{}", System.currentTimeMillis() / 1000); - } catch (IOException e) { - e.printStackTrace(); - } - //返回文本评分 - //处理模型提问逻辑 - String promptJson = ""; - //获取缓存记录 - String msgMapData = cacheMsgMapData.get(session.getId()); - if (StrUtil.isNotEmpty(msgMapData)) { - List list = JSONUtil.toList(msgMapData, Map.class); - //获取第一条数据记录 - Map mapEntity = list.get(0); - //更新问题记录 - mapEntity.put("role", "system"); - mapEntity.put("content", "You are a construction industry interview expert. Rate Construction Labourer candidate responses on a 1-5 scale. IMPORTANT: If the answer is completely unrelated, contains technical errors, system messages, or is nonsensical, give it a score of 0/5 and explain why it's invalid."); - promptJson = JSONUtil.toJsonStr(list); - //结束回答要清空问答数据 - cacheMsgMapData.put(session.getId(), ""); - } - log.info("结束AI提示词为:{}", promptJson); - ChatGPTClient gptClient = SpringUtils.getBean(ChatGPTClient.class); - String resultMsg = gptClient.handleAiChat(promptJson, "QA"); - Map resultEntity = new HashMap<>(); - resultEntity.put("content", resultMsg); - resultEntity.put("type", "score"); - session.getBasicRemote().sendText(JSONUtil.toJsonStr(resultEntity)); + log.info("面试结束啦!!!!!"); + handleInterviewEnd(session,""); } } } catch (Exception e) { @@ -415,7 +348,7 @@ public class ChatWebSocketHandler { List> list = new LinkedList(); Map mapEntity = new HashMap<>(); mapEntity.put("role", "system"); - mapEntity.put("content", "You are a construction industry interview expert. You MUST provide both an evaluation and follow-up questions for every response. Use this exact format:\\n\\nEVALUATION:\\n[Rate 0-5 and provide detailed assessment. If answer is unrelated, contains technical errors, system messages, or is nonsensical, give 0/5]\\n\\nFOLLOW-UP:\\n[Always generate 1-2 relevant follow-up questions. If answer was invalid, ask for clarification or repeat the question]\\n\\nIMPORTANT: You must include both EVALUATION and FOLLOW-UP sections in every response."); + mapEntity.put("content", "You are an interviewer. Generate in-depth follow-up questions based on candidate responses."); list.add(mapEntity); //获取预设问题-直接TTS转换返回语音结果 IHotakeProblemBaseInfoService problemBaseInfoService = SpringUtils.getBean(IHotakeProblemBaseInfoService.class); @@ -447,47 +380,54 @@ public class ChatWebSocketHandler { /** * 处理面试结束业务逻辑 - * 触发规则: - * 1、获得 0-1 分 大于1次 立即结束面试 - * 2、获取 4-5 分 大于3次 立即结束面试 - * 3、获取 2-3 分 大于3次 立即结束面试 - * 4、获取 2-5 分 大于4次 立即结束面试 - * 5、没有 FOLLOW-UP 理解结束面试 * * @param session 客户端会话 - * @param content 追问内容 + * @param position 职位 */ - private void handleInterviewEnd(Session session, String content) { - //验证是否触发面试结束逻辑 - try { - //发送面试官结束语音流 - String pathUrl = RuoYiConfig.getProfile() + VOICE_SYSTEM_DIR + "end.wav"; - sendVoiceBuffer(pathUrl, session); - //返回文本评分 + private void handleInterviewEnd(Session session,String position) { + //暂时的业务逻辑 + //发送面试官结束语音流 + String openingPathUrl = RuoYiConfig.getProfile() + VOICE_SYSTEM_DIR + "end.wav"; + sendVoiceBuffer(openingPathUrl, session); + + //返回文本评分 + //处理模型提问逻辑 + //获取缓存记录 + String msgMapData = cacheMsgMapData.get(session.getId()); + String promptJson = ""; + if (StrUtil.isNotEmpty(msgMapData)) { + List list = JSONUtil.toList(msgMapData, Map.class); + //获取第一条数据记录 + Map mapEntity = list.get(0); + //更新问题记录 + mapEntity.put("role", "system"); + mapEntity.put("content", "You are a construction industry interview expert. Rate candidate responses on a 1-5 scale and analyze key signals."); + //每个回答的内容前面要加上候选人的职位 + if (StrUtil.isNotEmpty(position)) { + for (Map map : list) { + if ("user".equals(map.get("role").toString())) { + map.put("content", "Position: " + position + "\\n" + map.get("content")); + } + } + } + promptJson = JSONUtil.toJsonStr(list); + //结束回答要清空问答数据 cacheMsgMapData.put(session.getId(), ""); - Map resultEntity = new HashMap<>(); - resultEntity.put("content", content); - resultEntity.put("type", "score"); + } + log.info("结束AI提示词为:{}", promptJson); + ChatGPTClient gptClient = SpringUtils.getBean(ChatGPTClient.class); + String resultMsg = gptClient.handleAiChat(promptJson, "QA"); + Map resultEntity = new HashMap<>(); + resultEntity.put("content", resultMsg); + resultEntity.put("type", "score"); + try{ session.getBasicRemote().sendText(JSONUtil.toJsonStr(resultEntity)); - - } catch (Exception e) { + }catch (Exception e){ e.printStackTrace(); } } - /** - * 解析AI结果,获取随机问题 - * - * @param content - * @return - */ - private String handleReturnQuestion(String content) { - - - return ""; - } - /** * 处理评分记录 * 触发规则: @@ -503,7 +443,7 @@ public class ChatWebSocketHandler { Map scoreRecordMap = cacheScoreResult.get(session.getId()); //对评分进行处理 if (StrUtil.isNotEmpty(content)) { - String[] strs = content.split("\\\\"); + String[] strs = content.split("/"); //取第一个数就是对应的评分 BigDecimal score = new BigDecimal(strs[0]); //记录Key为1 @@ -542,5 +482,57 @@ public class ChatWebSocketHandler { return true; } + /** + * 获取面试回答评分,并且校验是否结束面试 + * + * @param promptJson 提示词数据json + * @param session 客户端会话 + * @param position 职位 + */ + private Boolean getInterviewScore(String promptJson, Session session, String position) { + //返回文本评分 + //获取缓存记录 + String msgMapData = cacheMsgMapData.get(session.getId()); + if (StrUtil.isNotEmpty(msgMapData)) { + List list = JSONUtil.toList(msgMapData, Map.class); + //获取第一条数据记录 + Map mapEntity = list.get(0); + //更新问题记录 + mapEntity.put("role", "system"); + mapEntity.put("content", "You are a construction industry interview expert. Rate candidate responses on a 1-5 scale and analyze key signals."); + //每个回答的内容前面要加上候选人的职位 + if (StrUtil.isNotEmpty(position)) { + for (Map map : list) { + if ("user".equals(map.get("role").toString())) { + map.put("content", "Position: " + position + "\\n" + map.get("content")); + } + } + } + promptJson = JSONUtil.toJsonStr(list); + } + log.info("评分AI提示词为:{}", promptJson); + ChatGPTClient gptClient = SpringUtils.getBean(ChatGPTClient.class); + String resultMsg = gptClient.handleAiChat(promptJson, "QA"); + //评论格式为: Score: 3/5\nAssessment: Basically correct answer but lacks detail + String resultScore = ""; + if (StrUtil.isNotEmpty(resultMsg)) { + String[] resultMsgs = resultMsg.split("\\\\n"); + resultScore = resultMsgs[0].replaceAll(SCORE_FLAG, ""); + } + //校验面试是否结束 + Boolean flag = handleScoreRecord(resultScore, session); + try { + if (!flag) { + Map resultEntity = new HashMap<>(); + resultEntity.put("content", resultMsg); + resultEntity.put("type", "score"); + session.getBasicRemote().sendText(JSONUtil.toJsonStr(resultEntity)); + } + } catch (Exception e) { + e.printStackTrace(); + } + return flag; + } + } diff --git a/vetti-admin/src/main/resources/application-druid.yml b/vetti-admin/src/main/resources/application-druid.yml index ce69a14..8cae58d 100644 --- a/vetti-admin/src/main/resources/application-druid.yml +++ b/vetti-admin/src/main/resources/application-druid.yml @@ -169,12 +169,10 @@ whisper: chatGpt: apiKey: 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 apiUrl: https://api.openai.com/v1/chat/completions - model: ft:gpt-3.5-turbo-0125:vetti:construction-labourer-test:CWKBNvE2 - modelCV: ft:gpt-3.5-turbo-0125:vetti:vetti-resume-test:CWPinJQq + model: ft:gpt-3.5-turbo-0125:vetti::CYl9OBMN + modelCV: ft:gpt-3.5-turbo-0125:vetti:vetti-resume-full:CYT0C8JG role: system - - http: client: connect-timeout-seconds: 10 diff --git a/vetti-admin/src/main/resources/i18n/messages.properties b/vetti-admin/src/main/resources/i18n/messages.properties index c0ca2d0..e0f19b9 100644 --- a/vetti-admin/src/main/resources/i18n/messages.properties +++ b/vetti-admin/src/main/resources/i18n/messages.properties @@ -2,8 +2,8 @@ not.null=* 必须填写 user.jcaptcha.error=验证码错误 user.jcaptcha.expire=验证码已失效 -user.not.exists=用户不存在/密码错误 -user.password.not.match=用户不存在/密码错误 +user.not.exists=User does not exist/password incorrect +user.password.not.match=User does not exist/password incorrect user.password.retry.limit.count=密码输入错误{0}次 user.password.retry.limit.exceed=密码输入错误{0}次,帐户锁定{1}分钟 user.password.delete=对不起,您的账号已被删除 diff --git a/vetti-admin/target/classes/application-druid.yml b/vetti-admin/target/classes/application-druid.yml index ce69a14..8cae58d 100644 --- a/vetti-admin/target/classes/application-druid.yml +++ b/vetti-admin/target/classes/application-druid.yml @@ -169,12 +169,10 @@ whisper: chatGpt: apiKey: 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 apiUrl: https://api.openai.com/v1/chat/completions - model: ft:gpt-3.5-turbo-0125:vetti:construction-labourer-test:CWKBNvE2 - modelCV: ft:gpt-3.5-turbo-0125:vetti:vetti-resume-test:CWPinJQq + model: ft:gpt-3.5-turbo-0125:vetti::CYl9OBMN + modelCV: ft:gpt-3.5-turbo-0125:vetti:vetti-resume-full:CYT0C8JG role: system - - http: client: connect-timeout-seconds: 10 diff --git a/vetti-framework/src/main/java/com/vetti/framework/web/exception/GlobalExceptionHandler.java b/vetti-framework/src/main/java/com/vetti/framework/web/exception/GlobalExceptionHandler.java index 17a18f9..8390cd6 100644 --- a/vetti-framework/src/main/java/com/vetti/framework/web/exception/GlobalExceptionHandler.java +++ b/vetti-framework/src/main/java/com/vetti/framework/web/exception/GlobalExceptionHandler.java @@ -65,6 +65,9 @@ public class GlobalExceptionHandler { log.error(e.getMessage(), e); Integer code = e.getCode(); + if(code == null){ + code = HttpStatus.ERROR; + } response.setStatus(code); return StringUtils.isNotNull(code) ? AjaxResult.error(code, e.getMessage()) : AjaxResult.error(e.getMessage()); }