diff --git a/.kiro/steering/项目结构说明.md b/.kiro/steering/项目结构说明.md index 3855af6..0c572bf 100644 --- a/.kiro/steering/项目结构说明.md +++ b/.kiro/steering/项目结构说明.md @@ -69,7 +69,7 @@ offerpie/back-end │ ├─ aop/ # AOP 日志切面 │ ├─ exception/ # 业务异常统一处理 │ ├─ email/ # 邮件发送抽象(EmailAbility) -│ ├─ ai/ # AI 对话能力封装(AiChatAbility、AiChatConfig) +│ ├─ ai/ # AI 对话能力封装(AiChatAbility、AiChatConfig、AiResponseCleanTool) │ ├─ wxPay/ # 微信支付相关能力(Js、Native、Transfer 等) │ ├─ pojo/ # 公共 POJO(统一响应、登录/防重放 token 等) │ └─ web/ # Spring MVC 全局响应体 advice @@ -271,7 +271,7 @@ offerpie/back-end | **配置** | `OssConfig`, `RedissonConf`, `SecurityConfig`, `WxPayConfig`, `SmsConfig`, `AsyncConfig` | `common/config` | | **安全** | JWT 过滤器、登录令牌 (`RedisLoginTokenInfo`)、防重放 (`RedisPreventReplayInfo`) | `common/interceptor`、`common/pojo/interceptor` | | **邮件** | `EmailAbility`(封装邮件发送) | `common/email` | -| **AI** | `AiChatAbility`(OpenAI 兼容多供应商对话)、`AiChatConfig`(供应商配置) | `common/ai` | +| **AI** | `AiChatAbility`(OpenAI 兼容多供应商对话)、`AiChatConfig`(供应商配置)、`AiResponseCleanTool`(AI响应文本清洗) | `common/ai` | | **微信支付** | `WxJsPayAbility`, `WxNativePayAbility`, `WxTransferPayAbility`, `WxPayNotifyController` | `common/wxPay` | | **全局异常** | `GlobalExceptionAdvice`, `BusinessException`, `BusinessExpCodeEnum` | `common/exception` | | **日志 & AOP** | `ControllerLogAspect`, `LoggingOriginalRequestFilter`, `SqlLoggerInterceptor` | `common/aop`, `common/interceptor` | diff --git a/client-api/src/main/java/org/jiayunet/service/JobService.java b/client-api/src/main/java/org/jiayunet/service/JobService.java index 56f7154..4f97abf 100644 --- a/client-api/src/main/java/org/jiayunet/service/JobService.java +++ b/client-api/src/main/java/org/jiayunet/service/JobService.java @@ -1,6 +1,7 @@ package org.jiayunet.service; import org.jiayunet.ai.AiChatAbility; +import org.jiayunet.ai.AiResponseCleanTool; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import lombok.extern.slf4j.Slf4j; @@ -651,7 +652,7 @@ public class JobService { // 8. 调用AI String aiResponse = aiChatAbility.chat("job-recommend", systemPrompt, userMessage); - String json = cleanAiResponse(aiResponse); + String json = AiResponseCleanTool.clean(aiResponse); // 9. 解析AI返回,过滤出选中的岗位 try { @@ -675,13 +676,4 @@ public class JobService { } } - /** 清理AI返回的markdown代码块和控制字符 */ - private String cleanAiResponse(String response) { - String json = response.trim(); - if (json.startsWith("```")) { - json = json.replaceAll("^```\\w*\\n?", "").replaceAll("\\n?```$", "").trim(); - } - json = json.replaceAll("[\\x00-\\x08\\x0B\\x0C\\x0E-\\x1F]", ""); - return json; - } } diff --git a/common/src/main/java/org/jiayunet/ai/AiResponseCleanTool.java b/common/src/main/java/org/jiayunet/ai/AiResponseCleanTool.java new file mode 100644 index 0000000..7c044b0 --- /dev/null +++ b/common/src/main/java/org/jiayunet/ai/AiResponseCleanTool.java @@ -0,0 +1,58 @@ +package org.jiayunet.ai; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * AI 响应文本清洗工具 + *
从 AI 返回的原始文本中提取干净的 JSON 字符串,处理 markdown 代码块、前后说明文字、不可见控制字符等
+ * + * @author zk + */ +public final class AiResponseCleanTool { + + private AiResponseCleanTool() { + } + + /** 匹配 markdown 代码块:```可选语言标识\n 内容 \n```,DOTALL 让 . 匹配换行 */ + private static final Pattern CODE_BLOCK_PATTERN = Pattern.compile("```\\w*\\s*\\n?(.*?)\\n?\\s*```", Pattern.DOTALL); + + /** 匹配不可见控制字符(保留 \t \n \r) */ + private static final Pattern CONTROL_CHAR_PATTERN = Pattern.compile("[\\x00-\\x08\\x0B\\x0C\\x0E-\\x1F]"); + + /** + * 清理 AI 返回的文本,提取其中的 JSON + *1. null/空白返回空串 2. 优先从 markdown 代码块提取 3. 无代码块则定位首个 JSON 起始符 4. 清除控制字符
+ */ + public static String clean(String response) { + if (response == null || response.isBlank()) { + return ""; + } + String result = response.trim(); + + // 尝试从 markdown 代码块中提取 + Matcher matcher = CODE_BLOCK_PATTERN.matcher(result); + if (matcher.find()) { + result = matcher.group(1).trim(); + } else { + // 没有代码块时,尝试提取第一个 JSON 对象 {} 或数组 [] + int jsonStart = findJsonStart(result); + if (jsonStart > 0) { + result = result.substring(jsonStart).trim(); + } + } + + // 清除控制字符 + result = CONTROL_CHAR_PATTERN.matcher(result).replaceAll(""); + return result; + } + + /** 查找第一个 '{' 或 '[' 的位置,未找到返回 -1 */ + private static int findJsonStart(String text) { + int objStart = text.indexOf('{'); + int arrStart = text.indexOf('['); + if (objStart < 0) return arrStart; + if (arrStart < 0) return objStart; + return Math.min(objStart, arrStart); + } +} diff --git a/manager/src/main/java/org/jiayunet/service/JobCleanService.java b/manager/src/main/java/org/jiayunet/service/JobCleanService.java index 3007316..1dc082c 100644 --- a/manager/src/main/java/org/jiayunet/service/JobCleanService.java +++ b/manager/src/main/java/org/jiayunet/service/JobCleanService.java @@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.fasterxml.jackson.databind.JsonNode; import lombok.extern.slf4j.Slf4j; import org.jiayunet.ai.AiChatAbility; +import org.jiayunet.ai.AiResponseCleanTool; import com.baomidou.mybatisplus.core.toolkit.IdWorker; import org.jiayunet.mapper.AppJobDataMapper; import org.jiayunet.mapper.JobMapper; @@ -128,7 +129,7 @@ public class JobCleanService { // 3. 解析JSON try { - String json = cleanAiResponse(aiResponse); + String json = AiResponseCleanTool.clean(aiResponse); JsonNode root = HttpTool.objectMapper.readTree(json); // valid 校验 @@ -221,7 +222,7 @@ public class JobCleanService { "\n\n【专业分类列表】\n" + dictCacheService.getMajorCategoryText(); String aiResponse = aiChatAbility.chat(systemPrompt, userMessage); - String json = cleanAiResponse(aiResponse); + String json = AiResponseCleanTool.clean(aiResponse); try { JsonNode root = HttpTool.objectMapper.readTree(json); @@ -280,7 +281,7 @@ public class JobCleanService { String userMessage = "【岗位信息】\n标题: " + title + "\n职责: " + description + "\n要求: " + requirement; String aiResponse = aiChatAbility.chat(systemPrompt, userMessage); - String json = cleanAiResponse(aiResponse); + String json = AiResponseCleanTool.clean(aiResponse); try { JsonNode arrayNode = HttpTool.objectMapper.readTree(json); @@ -377,16 +378,6 @@ public class JobCleanService { return sb.toString(); } - /** 清理AI返回的markdown代码块和控制字符 */ - private String cleanAiResponse(String response) { - String json = response.trim(); - if (json.startsWith("```")) { - json = json.replaceAll("^```\\w*\\n?", "").replaceAll("\\n?```$", "").trim(); - } - json = json.replaceAll("[\\x00-\\x08\\x0B\\x0C\\x0E-\\x1F]", ""); - return json; - } - private String nullToEmpty(String s) { return s == null ? "" : s; } diff --git a/manager/src/main/java/org/jiayunet/service/UserProfileAnalyzeService.java b/manager/src/main/java/org/jiayunet/service/UserProfileAnalyzeService.java index 72cbf22..1e37fb7 100644 --- a/manager/src/main/java/org/jiayunet/service/UserProfileAnalyzeService.java +++ b/manager/src/main/java/org/jiayunet/service/UserProfileAnalyzeService.java @@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.core.toolkit.IdWorker; import com.fasterxml.jackson.databind.JsonNode; import lombok.extern.slf4j.Slf4j; import org.jiayunet.ai.AiChatAbility; +import org.jiayunet.ai.AiResponseCleanTool; import org.jiayunet.mapper.*; import org.jiayunet.pojo.po.*; import org.jiayunet.pojo.vo.UserHonorsVo; @@ -145,7 +146,7 @@ public class UserProfileAnalyzeService { String userMessage = "【用户简历】\n" + profileJson; String aiResponse = aiChatAbility.chat(systemPrompt, userMessage); - String json = cleanAiResponse(aiResponse); + String json = AiResponseCleanTool.clean(aiResponse); try { JsonNode root = HttpTool.objectMapper.readTree(json); @@ -193,7 +194,7 @@ public class UserProfileAnalyzeService { String userMessage = "【用户简历】\n" + profileJson + "\n\n【专业分类列表】\n" + dictCacheService.getMajorCategoryText(); String aiResponse = aiChatAbility.chat(systemPrompt, userMessage); - String json = cleanAiResponse(aiResponse); + String json = AiResponseCleanTool.clean(aiResponse); try { JsonNode root = HttpTool.objectMapper.readTree(json); @@ -253,7 +254,7 @@ public class UserProfileAnalyzeService { String userMessage = "【用户简历】\n" + profileJson; String aiResponse = aiChatAbility.chat(systemPrompt, userMessage); - String json = cleanAiResponse(aiResponse); + String json = AiResponseCleanTool.clean(aiResponse); try { JsonNode arrayNode = HttpTool.objectMapper.readTree(json); @@ -332,13 +333,4 @@ public class UserProfileAnalyzeService { relationMapper.batchInsert(relations); } - /** 清理AI返回的markdown代码块和控制字符 */ - private String cleanAiResponse(String response) { - String json = response.trim(); - if (json.startsWith("```")) { - json = json.replaceAll("^```\\w*\\n?", "").replaceAll("\\n?```$", "").trim(); - } - json = json.replaceAll("[\\x00-\\x08\\x0B\\x0C\\x0E-\\x1F]", ""); - return json; - } }