Files

1 line
127 KiB
JSON
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{"repo": "al-say/automatedresumes", "files": {"src/main/java/com/getjobs/application/service/AiService.java": {"f15092fe12e70daeaca24e07572494639513958c": {"t": 1785745698.7344992, "n": 17341, "h": "ed313b7834144ec7", "c": "package com.getjobs.application.service;\n\nimport com.getjobs.application.entity.AiEntity;\nimport com.getjobs.application.mapper.AiMapper;\nimport lombok.RequiredArgsConstructor;\nimport lombok.extern.slf4j.Slf4j;\nimport org.json.JSONArray;\nimport org.json.JSONObject;\nimport org.springframework.stereotype.Service;\nimport org.springframework.transaction.annotation.Transactional;\n\nimport java.net.URI;\nimport java.net.http.HttpClient;\nimport java.net.http.HttpRequest;\nimport java.net.http.HttpResponse;\nimport java.time.Duration;\nimport java.time.Instant;\nimport java.time.LocalDateTime;\nimport java.time.ZoneId;\nimport java.time.format.DateTimeFormatter;\n\n/**\n * AI 服务(Spring 管理)\n * 从数据库配置获取 BASE_URL、API_KEY、MODEL 并发起 AI 请求。\n */\n@Service\n@Slf4j\n@RequiredArgsConstructor\npublic class AiService {\n private final ConfigService configService;\n private final AiMapper aiMapper;\n\n /**\n * 隐藏API密钥,只显示前6位和后4位\n * @param apiKey 原始API密钥\n * @return 隐藏后的密钥字符串\n */\n private String maskApiKey(String apiKey) {\n if (apiKey == null || apiKey.length() < 8) {\n return \"INVALID_KEY\";\n }\n return apiKey.substring(0, 6) + \"********\" + apiKey.substring(apiKey.length() - 4);\n }\n\n /**\n * 发送 AI 请求(非流式)并返回回复内容。\n * @param content 用户消息内容\n * @return AI 回复文本\n */\n public String sendRequest(String content) {\n // 优先从环境变量读取智谱AI配置,如果没有则从数据库读取\n String baseUrl = System.getenv(\"ZHIPU_BASE_URL\");\n String apiKey = System.getenv(\"ZHIPU_API_KEY\");\n String model = System.getenv(\"ZHIPU_MODEL\");\n\n // 如果智谱环境变量不存在,尝试DeepSeek配置\n if (baseUrl == null || apiKey == null) {\n baseUrl = System.getenv(\"DEEPSEEK_BASE_URL\");\n apiKey = System.getenv(\"DEEPSEEK_API_KEY\");\n model = System.getenv(\"DEEPSEEK_MODEL\");\n }\n\n // 如果环境变量不存在,则回退到数据库配置\n if (baseUrl == null || apiKey == null) {\n var cfg = configService.getAiConfigs();\n baseUrl = baseUrl != null ? baseUrl : cfg.get(\"BASE_URL\");\n apiKey = apiKey != null ? apiKey : cfg.get(\"API_KEY\");\n model = model != null ? model : cfg.get(\"MODEL\");\n }\n\n // 如果仍然没有配置,使用智谱默认值\n if (baseUrl == null) baseUrl = \"https://open.bigmodel.cn/api/paas/v4\";\n if (model == null) model = \"glm-4\";\n\n // 校验必需参数\n if (apiKey == null || apiKey.trim().isEmpty()) {\n throw new RuntimeException(\"AI API Key 未配置,请设置 ZHIPU_API_KEY 或 DEEPSEEK_API_KEY 环境变量或数据库配置\");\n }\n\n log.info(\"使用AI配置 - BaseURL: {}, Model: {}, Key: {}...\",\n baseUrl, model, maskApiKey(apiKey));\n\n // 根据模型类型选择兼容的端点(部分“推理/Reasoning”模型需要使用 Responses API)\n String endpoint = isResponsesModel(model)\n ? buildResponsesEndpoint(baseUrl)\n : buildChatCompletionsEndpoint(baseUrl);\n\n int timeoutInSeconds = 60;\n\n HttpClient client = HttpClient.newBuilder()\n .connectTimeout(Duration.ofSeconds(timeoutInSeconds))\n .build();\n\n // 构建 JSON 请求体\n JSONObject requestData = new JSONObject();\n requestData.put(\"model\", model);\n requestData.put(\"temperature\", 0.5);\n if (endpoint.endsWith(\"/responses\")) {\n // Responses API 采用 input 字段\n requestData.put(\"input\", content);\n // 如需显式控制推理强度,可按需开启:\n // JSONObject reasoning = new JSONObject();\n // reasoning.put(\"effort\", \"medium\");\n // requestData.put(\"reasoning\", reasoning);\n } else {\n // Chat Completions API 使用 messages\n JSONArray messages = new JSONArray();\n JSONObject message = new JSONObject();\n message.put(\"role\", \"user\");\n message.put(\"content\", content);\n messages.put(message);\n requestData.put(\"messages\", messages);\n }\n\n HttpRequest request = HttpRequest.newBuilder()\n .uri(URI.create(endpoint))\n .header(\"Content-Type\", \"application/json\")\n .header(\"Accept\", \"application/json\")\n .header(\"Authorization\", \"Bearer \" + apiKey)\n // 某些服务(例如 Azure OpenAI)需要 api-key 头,额外加一层兼容\n .header(\"api-key\", apiKey)\n .POST(HttpRequest.BodyPublishers.ofString(requestData.toString()))\n .build();\n\n try {\n HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());\n if (response.statusCode() == 200) {\n JSONObject responseObject = new JSONObject(response.body());\n\n String requestId = responseObject.optString(\"id\");\n long created = responseObject.optLong(\"created\", 0);\n String usedModel = responseObject.optString(\"model\");\n\n String responseContent;\n if (endpoint.endsWith(\"/responses\")) {\n // Responses API:优先读取 output_text\n responseContent = responseObject.optString(\"output_text\", null);\n if (responseContent == null || responseContent.isEmpty()) {\n // 兜底:尝试从通用 choices/message 结构读取(部分代理/兼容层会返回该结构)\n try {\n JSONObject messageObject = responseObject.getJSONArray(\"choices\")\n .getJSONObject(0)\n .getJSONObject(\"message\");\n responseContent = messageObject.getString(\"content\");\n } catch (Exception ignore) {\n responseContent = response.body(); // 最后兜底:返回原始文本,避免空值\n }\n }\n } else {\n // Chat Completions API\n JSONObject messageObject = responseObject.getJSONArray(\"choices\")\n .getJSONObject(0)\n .getJSONObject(\"message\");\n responseContent = messageObject.getString(\"content\");\n }\n\n JSONObject usageObject = responseObject.optJSONObject(\"usage\");\n int promptTokens = usageObject != null ? usageObject.optInt(\"prompt_tokens\", -1) : -1;\n int completionTokens = usageObject != null ? usageObject.optInt(\"completion_tokens\", -1) : -1;\n int totalTokens = usageObject != null ? usageObject.optInt(\"total_tokens\", -1) : -1;\n\n LocalDateTime createdTime = created > 0\n ? Instant.ofEpochSecond(created).atZone(ZoneId.systemDefault()).toLocalDateTime()\n : LocalDateTime.now();\n DateTimeFormatter formatter = DateTimeFormatter.ofPattern(\"yyyy-MM-dd HH:mm:ss\");\n\n log.info(\"AI响应: id={}, time={}, model={}, promptTokens={}, completionTokens={}, totalTokens={}\",\n requestId, createdTime.format(formatter), usedModel, promptTokens, completionTokens, totalTokens);\n\n return responseContent;\n } else {\n // 更详细的错误日志,便于定位 400 问题\n log.error(\"AI请求失败: status={}, endpoint={}, body={}\", response.statusCode(), endpoint, response.body());\n // 针对 Responses-only 模型误用 Chat Completions 的常见错误做一次自动重试\n if (!endpoint.endsWith(\"/responses\") && containsReasoningParamError(response.body())) {\n String fallbackEndpoint = buildResponsesEndpoint(baseUrl);\n log.warn(\"检测到 reasoning 相关参数错误,自动切换到 Responses API 重试: {}\", fallbackEndpoint);\n return sendRequestViaResponses(content, apiKey, model, fallbackEndpoint);\n }\n throw new RuntimeException(\"AI请求失败,状态码: \" + response.statusCode() + \", 详情: \" + response.body());\n }\n } catch (Exception e) {\n log.error(\"调用AI服务异常\", e);\n throw e instanceof RuntimeException ? (RuntimeException) e : new RuntimeException(e);\n }\n }\n\n private String normalizeBaseUrl(String baseUrl) {\n if (baseUrl == null) return \"\";\n String trimmed = baseUrl.trim();\n return trimmed.endsWith(\"/\") ? trimmed.substring(0, trimmed.length() - 1) : trimmed;\n }\n\n /**\n * 根据配置构造 chat/completions 端点,避免重复拼接 /v1\n */\n private String buildChatCompletionsEndpoint(String baseUrl) {\n String normalized = normalizeBaseUrl(baseUrl);\n\n // 特殊处理智谱AI:智谱AI的baseUrl已经是 https://open.bigmodel.cn/api/paas/v4\n // 不需要再拼接 /v1,直接拼接 /chat/completions\n if (normalized.contains(\"open.bigmodel.cn\")) {\n return normalized + \"/chat/completions\";\n }\n\n // 其他服务:如果 baseUrl 已经包含 /v1(常见配置为 https://api.openai.com/v1),则只拼接 /chat/completions\n if (normalized.endsWith(\"/v1\") || normalized.contains(\"/v1/\")) {\n return normalized + \"/chat/completions\";\n }\n return normalized + \"/v1/chat/completions\";\n }\n\n /**\n * 构造 Responses API 端点\n */\n private String buildResponsesEndpoint(String baseUrl) {\n String normalized = normalizeBaseUrl(baseUrl);\n if (normalized.endsWith(\"/v1\") || normalized.contains(\"/v1/\")) {\n return normalized + \"/responses\";\n }\n return normalized + \"/v1/responses\";\n }\n\n /**\n * 粗略识别需要使用 Responses API 的模型(o-系列、4.1、reasoner 等)\n */\n private boolean isResponsesModel(String model) {\n if (model == null) return false;\n String m = model.toLowerCase();\n return m.contains(\"o1\") || m.contains(\"o3\") || m.contains(\"o4\")\n || m.contains(\"4.1\") || m.contains(\"reasoner\")\n || m.contains(\"4o-mini\") || m.contains(\"gpt-4o-mini\");\n }\n\n /**\n * 检查错误响应中是否包含 reasoning 相关参数错误(如 reasoning.summary unsupported_value)\n */\n private boolean containsReasoningParamError(String body) {\n if (body == null) return false;\n String s = body.toLowerCase();\n return (s.contains(\"reasoning\") && s.contains(\"unsupported_value\"))\n || s.contains(\"reasoning.summary\");\n }\n\n /**\n * 使用 Responses API 发送一次请求(用于自动降级/重试)\n */\n private String sendRequestViaResponses(String content, String apiKey, String model, String endpoint) {\n int timeoutInSeconds = 60;\n HttpClient client = HttpClient.newBuilder()\n .connectTimeout(Duration.ofSeconds(timeoutInSeconds))\n .build();\n\n JSONObject requestData = new JSONObject();\n requestData.put(\"model\", model);\n requestData.put(\"temperature\", 0.5);\n requestData.put(\"input\", content);\n\n HttpRequest request = HttpRequest.newBuilder()\n .uri(URI.create(endpoint))\n .header(\"Content-Type\", \"application/json\")\n .header(\"Accept\", \"application/json\")\n .header(\"Authorization\", \"Bearer \" + apiKey)\n .header(\"api-key\", apiKey)\n .POST(HttpRequest.BodyPublishers.ofString(requestData.toString()))\n .build();\n\n try {\n HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());\n if (response.statusCode() == 200) {\n JSONObject resp = new JSONObject(response.body());\n String outputText = resp.optString(\"output_text\", null);\n if (outputText != null && !outputText.isEmpty()) {\n return outputText;\n }\n // 兜底解析:部分兼容层可能返回 choices/message 结构\n try {\n JSONObject messageObject = resp.getJSONArray(\"choices\")\n .getJSONObject(0)\n .getJSONObject(\"message\");\n return messageObject.getString(\"content\");\n } catch (Exception ignore) {\n }\n // 无法解析则直接返回原始体,避免空值中断流程\n return response.body();\n }\n log.error(\"Responses API 调用失败: status={}, endpoint={}, body={}\", response.statusCode(), endpoint, response.body());\n throw new RuntimeException(\"AI请求失败,状态码: \" + response.statusCode() + \", 详情: \" + response.body());\n } catch (Exception e) {\n log.error(\"Responses API 调用异常\", e);\n throw e instanceof RuntimeException ? (RuntimeException) e : new RuntimeException(e);\n }\n }\n\n // ================= 合并的 AI 配置管理方法 =================\n\n /**\n * 获取AI配置(获取最新一条,如果不存在则创建默认配置)\n */\n @Transactional(readOnly = true)\n public AiEntity getAiConfig() {\n var list = aiMapper.selectList(null);\n AiEntity aiEntity = (list == null || list.isEmpty()) ? null : list.get(list.size() - 1);\n if (aiEntity == null) {\n aiEntity = createDefaultConfig();\n }\n return aiEntity;\n }\n\n /**\n * 获取所有AI配置\n */\n @Transactional(readOnly = true)\n public java.util.List<AiEntity> getAllAiConfigs() {\n return aiMapper.selectList(null);\n }\n\n /**\n * 根据ID获取AI配置\n */\n @Transactional(readOnly = true)\n public AiEntity getAiConfigById(Long id) {\n return aiMapper.selectById(id);\n }\n\n /**\n * 保存或更新AI配置(introduce/prompt)\n */\n @Transactional\n public AiEntity saveOrUpdateAiConfig(String introduce, String prompt) {\n var list = aiMapper.selectList(null);\n AiEntity aiEntity = (list == null || list.isEmpty()) ? null : list.get(list.size() - 1);\n\n if (aiEntity == null) {\n aiEntity = new AiEntity();\n aiEntity.setIntroduce(introduce);\n aiEntity.setPrompt(prompt);\n aiEntity.setCreatedAt(java.time.LocalDateTime.now());\n aiEntity.setUpdatedAt(java.time.LocalDateTime.now());\n aiMapper.insert(aiEntity);\n log.info(\"创建新的AI配置,ID: {}\", aiEntity.getId());\n } else {\n aiEntity.setIntroduce(introduce);\n aiEntity.setPrompt(prompt);\n aiEntity.setUpdatedAt(java.time.LocalDateTime.now());\n aiMapper.updateById(aiEntity);\n log.info(\"更新AI配置,ID: {}\", aiEntity.getId());\n }\n\n return aiEntity;\n }\n\n /**\n * 删除AI配置\n */\n @Transactional\n public boolean deleteAiConfig(Long id) {\n int result = aiMapper.deleteById(id);\n if (result > 0) {\n log.info(\"删除AI配置成功,ID: {}\", id);\n return true;\n }\n return false;\n }\n\n /**\n * 创建默认配置\n */\n @Transactional\n protected AiEntity createDefaultConfig() {\n AiEntity aiEntity = new AiEntity();\n aiEntity.setIntroduce(\"请在此填写您的技能介绍\");\n aiEntity.setPrompt(\"请在此填写AI提示词模板\");\n aiEntity.setCreatedAt(java.time.LocalDateTime.now());\n aiEntity.setUpdatedAt(java.time.LocalDateTime.now());\n aiMapper.insert(aiEntity);\n log.info(\"创建默认AI配置,ID: {}\", aiEntity.getId());\n return aiEntity;\n }\n\n /**\n * 测试AI连接(支持智谱和DeepSeek)\n * @return 测试结果\n */\n public String testDeepSeekConnection() {\n try {\n String testMessage = \"你好,请简单回复一个字:是\";\n String response = sendRequest(testMessage);\n return \"AI连接成功,响应: \" + response;\n } catch (Exception e) {\n log.error(\"AI连接测试失败\", e);\n return \"AI连接失败: \" + e.getMessage();\n }\n }\n\n /**\n * 获取当前使用的AI配置信息(用于调试)\n * @return 配置信息字符串\n */\n public String getCurrentAiConfig() {\n StringBuilder config = new StringBuilder();\n config.append(\"AI配置信息:\\n\");\n\n // 智谱AI环境变量配置\n String zhipuBaseUrl = System.getenv(\"ZHIPU_BASE_URL\");\n String zhipuApiKey = System.getenv(\"ZHIPU_API_KEY\");\n String zhipuModel = System.getenv(\"ZHIPU_MODEL\");\n\n if (zhipuBaseUrl != null || zhipuApiKey != null) {\n config.append(\"- 智谱AI环境变量配置:\\n\");\n config.append(\" ZHIPU_BASE_URL: \").append(zhipuBaseUrl != null ? zhipuBaseUrl : \"未设置\").append(\"\\n\");\n config.append(\" ZHIPU_API_KEY: \").append(zhipuApiKey != null ? maskApiKey(zhipuApiKey) : \"未设置\").append(\"\\n\");\n config.append(\" ZHIPU_MODEL: \").append(zhipuModel != null ? zhipuModel : \"未设置\").append(\"\\n\");\n }\n\n // DeepSeek环境变量配置\n String envBaseUrl = System.getenv(\"DEEPSEEK_BASE_URL\");\n String envApiKey = System.getenv(\"DEEPSEEK_API_KEY\");\n String envModel = System.getenv(\"DEEPSEEK_MODEL\");\n\n if (envBaseUrl != null || envApiKey != null) {\n config.append(\"- DeepSeek环境变量配置:\\n\");\n config.append(\" DEEPSEEK_BASE_URL: \").append(envBaseUrl != null ? envBaseUrl : \"未设置\").append(\"\\n\");\n config.append(\" DEEPSEEK_API_KEY: \").append(envApiKey != null ? maskApiKey(envApiKey) : \"未设置\").append(\"\\n\");\n config.append(\" DEEPSEEK_MODEL: \").append(envModel != null ? envModel : \"未设置\").append(\"\\n\");\n }\n\n // 数据库配置(回退选项)\n try {\n var cfg = configService.getAiConfigs();\n config.append(\"- 数据库配置:\\n\");\n config.append(\" BASE_URL: \").append(cfg.get(\"BASE_URL\")).append(\"\\n\");\n config.append(\" API_KEY: \").append(maskApiKey(cfg.get(\"API_KEY\"))).append(\"\\n\");\n config.append(\" MODEL: \").append(cfg.get(\"MODEL\")).append(\"\\n\");\n } catch (Exception e) {\n config.append(\"- 数据库配置: 读取失败\\n\");\n }\n\n return config.toString();\n }\n}"}, "36a1891251bb1f104e534c8a3d5e81600c635c09": {"t": 1785755752.1560857, "n": 17341, "h": "ed313b7834144ec7", "c": "package com.getjobs.application.service;\n\nimport com.getjobs.application.entity.AiEntity;\nimport com.getjobs.application.mapper.AiMapper;\nimport lombok.RequiredArgsConstructor;\nimport lombok.extern.slf4j.Slf4j;\nimport org.json.JSONArray;\nimport org.json.JSONObject;\nimport org.springframework.stereotype.Service;\nimport org.springframework.transaction.annotation.Transactional;\n\nimport java.net.URI;\nimport java.net.http.HttpClient;\nimport java.net.http.HttpRequest;\nimport java.net.http.HttpResponse;\nimport java.time.Duration;\nimport java.time.Instant;\nimport java.time.LocalDateTime;\nimport java.time.ZoneId;\nimport java.time.format.DateTimeFormatter;\n\n/**\n * AI 服务(Spring 管理)\n * 从数据库配置获取 BASE_URL、API_KEY、MODEL 并发起 AI 请求。\n */\n@Service\n@Slf4j\n@RequiredArgsConstructor\npublic class AiService {\n private final ConfigService configService;\n private final AiMapper aiMapper;\n\n /**\n * 隐藏API密钥,只显示前6位和后4位\n * @param apiKey 原始API密钥\n * @return 隐藏后的密钥字符串\n */\n private String maskApiKey(String apiKey) {\n if (apiKey == null || apiKey.length() < 8) {\n return \"INVALID_KEY\";\n }\n return apiKey.substring(0, 6) + \"********\" + apiKey.substring(apiKey.length() - 4);\n }\n\n /**\n * 发送 AI 请求(非流式)并返回回复内容。\n * @param content 用户消息内容\n * @return AI 回复文本\n */\n public String sendRequest(String content) {\n // 优先从环境变量读取智谱AI配置,如果没有则从数据库读取\n String baseUrl = System.getenv(\"ZHIPU_BASE_URL\");\n String apiKey = System.getenv(\"ZHIPU_API_KEY\");\n String model = System.getenv(\"ZHIPU_MODEL\");\n\n // 如果智谱环境变量不存在,尝试DeepSeek配置\n if (baseUrl == null || apiKey == null) {\n baseUrl = System.getenv(\"DEEPSEEK_BASE_URL\");\n apiKey = System.getenv(\"DEEPSEEK_API_KEY\");\n model = System.getenv(\"DEEPSEEK_MODEL\");\n }\n\n // 如果环境变量不存在,则回退到数据库配置\n if (baseUrl == null || apiKey == null) {\n var cfg = configService.getAiConfigs();\n baseUrl = baseUrl != null ? baseUrl : cfg.get(\"BASE_URL\");\n apiKey = apiKey != null ? apiKey : cfg.get(\"API_KEY\");\n model = model != null ? model : cfg.get(\"MODEL\");\n }\n\n // 如果仍然没有配置,使用智谱默认值\n if (baseUrl == null) baseUrl = \"https://open.bigmodel.cn/api/paas/v4\";\n if (model == null) model = \"glm-4\";\n\n // 校验必需参数\n if (apiKey == null || apiKey.trim().isEmpty()) {\n throw new RuntimeException(\"AI API Key 未配置,请设置 ZHIPU_API_KEY 或 DEEPSEEK_API_KEY 环境变量或数据库配置\");\n }\n\n log.info(\"使用AI配置 - BaseURL: {}, Model: {}, Key: {}...\",\n baseUrl, model, maskApiKey(apiKey));\n\n // 根据模型类型选择兼容的端点(部分“推理/Reasoning”模型需要使用 Responses API)\n String endpoint = isResponsesModel(model)\n ? buildResponsesEndpoint(baseUrl)\n : buildChatCompletionsEndpoint(baseUrl);\n\n int timeoutInSeconds = 60;\n\n HttpClient client = HttpClient.newBuilder()\n .connectTimeout(Duration.ofSeconds(timeoutInSeconds))\n .build();\n\n // 构建 JSON 请求体\n JSONObject requestData = new JSONObject();\n requestData.put(\"model\", model);\n requestData.put(\"temperature\", 0.5);\n if (endpoint.endsWith(\"/responses\")) {\n // Responses API 采用 input 字段\n requestData.put(\"input\", content);\n // 如需显式控制推理强度,可按需开启:\n // JSONObject reasoning = new JSONObject();\n // reasoning.put(\"effort\", \"medium\");\n // requestData.put(\"reasoning\", reasoning);\n } else {\n // Chat Completions API 使用 messages\n JSONArray messages = new JSONArray();\n JSONObject message = new JSONObject();\n message.put(\"role\", \"user\");\n message.put(\"content\", content);\n messages.put(message);\n requestData.put(\"messages\", messages);\n }\n\n HttpRequest request = HttpRequest.newBuilder()\n .uri(URI.create(endpoint))\n .header(\"Content-Type\", \"application/json\")\n .header(\"Accept\", \"application/json\")\n .header(\"Authorization\", \"Bearer \" + apiKey)\n // 某些服务(例如 Azure OpenAI)需要 api-key 头,额外加一层兼容\n .header(\"api-key\", apiKey)\n .POST(HttpRequest.BodyPublishers.ofString(requestData.toString()))\n .build();\n\n try {\n HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());\n if (response.statusCode() == 200) {\n JSONObject responseObject = new JSONObject(response.body());\n\n String requestId = responseObject.optString(\"id\");\n long created = responseObject.optLong(\"created\", 0);\n String usedModel = responseObject.optString(\"model\");\n\n String responseContent;\n if (endpoint.endsWith(\"/responses\")) {\n // Responses API:优先读取 output_text\n responseContent = responseObject.optString(\"output_text\", null);\n if (responseContent == null || responseContent.isEmpty()) {\n // 兜底:尝试从通用 choices/message 结构读取(部分代理/兼容层会返回该结构)\n try {\n JSONObject messageObject = responseObject.getJSONArray(\"choices\")\n .getJSONObject(0)\n .getJSONObject(\"message\");\n responseContent = messageObject.getString(\"content\");\n } catch (Exception ignore) {\n responseContent = response.body(); // 最后兜底:返回原始文本,避免空值\n }\n }\n } else {\n // Chat Completions API\n JSONObject messageObject = responseObject.getJSONArray(\"choices\")\n .getJSONObject(0)\n .getJSONObject(\"message\");\n responseContent = messageObject.getString(\"content\");\n }\n\n JSONObject usageObject = responseObject.optJSONObject(\"usage\");\n int promptTokens = usageObject != null ? usageObject.optInt(\"prompt_tokens\", -1) : -1;\n int completionTokens = usageObject != null ? usageObject.optInt(\"completion_tokens\", -1) : -1;\n int totalTokens = usageObject != null ? usageObject.optInt(\"total_tokens\", -1) : -1;\n\n LocalDateTime createdTime = created > 0\n ? Instant.ofEpochSecond(created).atZone(ZoneId.systemDefault()).toLocalDateTime()\n : LocalDateTime.now();\n DateTimeFormatter formatter = DateTimeFormatter.ofPattern(\"yyyy-MM-dd HH:mm:ss\");\n\n log.info(\"AI响应: id={}, time={}, model={}, promptTokens={}, completionTokens={}, totalTokens={}\",\n requestId, createdTime.format(formatter), usedModel, promptTokens, completionTokens, totalTokens);\n\n return responseContent;\n } else {\n // 更详细的错误日志,便于定位 400 问题\n log.error(\"AI请求失败: status={}, endpoint={}, body={}\", response.statusCode(), endpoint, response.body());\n // 针对 Responses-only 模型误用 Chat Completions 的常见错误做一次自动重试\n if (!endpoint.endsWith(\"/responses\") && containsReasoningParamError(response.body())) {\n String fallbackEndpoint = buildResponsesEndpoint(baseUrl);\n log.warn(\"检测到 reasoning 相关参数错误,自动切换到 Responses API 重试: {}\", fallbackEndpoint);\n return sendRequestViaResponses(content, apiKey, model, fallbackEndpoint);\n }\n throw new RuntimeException(\"AI请求失败,状态码: \" + response.statusCode() + \", 详情: \" + response.body());\n }\n } catch (Exception e) {\n log.error(\"调用AI服务异常\", e);\n throw e instanceof RuntimeException ? (RuntimeException) e : new RuntimeException(e);\n }\n }\n\n private String normalizeBaseUrl(String baseUrl) {\n if (baseUrl == null) return \"\";\n String trimmed = baseUrl.trim();\n return trimmed.endsWith(\"/\") ? trimmed.substring(0, trimmed.length() - 1) : trimmed;\n }\n\n /**\n * 根据配置构造 chat/completions 端点,避免重复拼接 /v1\n */\n private String buildChatCompletionsEndpoint(String baseUrl) {\n String normalized = normalizeBaseUrl(baseUrl);\n\n // 特殊处理智谱AI:智谱AI的baseUrl已经是 https://open.bigmodel.cn/api/paas/v4\n // 不需要再拼接 /v1,直接拼接 /chat/completions\n if (normalized.contains(\"open.bigmodel.cn\")) {\n return normalized + \"/chat/completions\";\n }\n\n // 其他服务:如果 baseUrl 已经包含 /v1(常见配置为 https://api.openai.com/v1),则只拼接 /chat/completions\n if (normalized.endsWith(\"/v1\") || normalized.contains(\"/v1/\")) {\n return normalized + \"/chat/completions\";\n }\n return normalized + \"/v1/chat/completions\";\n }\n\n /**\n * 构造 Responses API 端点\n */\n private String buildResponsesEndpoint(String baseUrl) {\n String normalized = normalizeBaseUrl(baseUrl);\n if (normalized.endsWith(\"/v1\") || normalized.contains(\"/v1/\")) {\n return normalized + \"/responses\";\n }\n return normalized + \"/v1/responses\";\n }\n\n /**\n * 粗略识别需要使用 Responses API 的模型(o-系列、4.1、reasoner 等)\n */\n private boolean isResponsesModel(String model) {\n if (model == null) return false;\n String m = model.toLowerCase();\n return m.contains(\"o1\") || m.contains(\"o3\") || m.contains(\"o4\")\n || m.contains(\"4.1\") || m.contains(\"reasoner\")\n || m.contains(\"4o-mini\") || m.contains(\"gpt-4o-mini\");\n }\n\n /**\n * 检查错误响应中是否包含 reasoning 相关参数错误(如 reasoning.summary unsupported_value)\n */\n private boolean containsReasoningParamError(String body) {\n if (body == null) return false;\n String s = body.toLowerCase();\n return (s.contains(\"reasoning\") && s.contains(\"unsupported_value\"))\n || s.contains(\"reasoning.summary\");\n }\n\n /**\n * 使用 Responses API 发送一次请求(用于自动降级/重试)\n */\n private String sendRequestViaResponses(String content, String apiKey, String model, String endpoint) {\n int timeoutInSeconds = 60;\n HttpClient client = HttpClient.newBuilder()\n .connectTimeout(Duration.ofSeconds(timeoutInSeconds))\n .build();\n\n JSONObject requestData = new JSONObject();\n requestData.put(\"model\", model);\n requestData.put(\"temperature\", 0.5);\n requestData.put(\"input\", content);\n\n HttpRequest request = HttpRequest.newBuilder()\n .uri(URI.create(endpoint))\n .header(\"Content-Type\", \"application/json\")\n .header(\"Accept\", \"application/json\")\n .header(\"Authorization\", \"Bearer \" + apiKey)\n .header(\"api-key\", apiKey)\n .POST(HttpRequest.BodyPublishers.ofString(requestData.toString()))\n .build();\n\n try {\n HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());\n if (response.statusCode() == 200) {\n JSONObject resp = new JSONObject(response.body());\n String outputText = resp.optString(\"output_text\", null);\n if (outputText != null && !outputText.isEmpty()) {\n return outputText;\n }\n // 兜底解析:部分兼容层可能返回 choices/message 结构\n try {\n JSONObject messageObject = resp.getJSONArray(\"choices\")\n .getJSONObject(0)\n .getJSONObject(\"message\");\n return messageObject.getString(\"content\");\n } catch (Exception ignore) {\n }\n // 无法解析则直接返回原始体,避免空值中断流程\n return response.body();\n }\n log.error(\"Responses API 调用失败: status={}, endpoint={}, body={}\", response.statusCode(), endpoint, response.body());\n throw new RuntimeException(\"AI请求失败,状态码: \" + response.statusCode() + \", 详情: \" + response.body());\n } catch (Exception e) {\n log.error(\"Responses API 调用异常\", e);\n throw e instanceof RuntimeException ? (RuntimeException) e : new RuntimeException(e);\n }\n }\n\n // ================= 合并的 AI 配置管理方法 =================\n\n /**\n * 获取AI配置(获取最新一条,如果不存在则创建默认配置)\n */\n @Transactional(readOnly = true)\n public AiEntity getAiConfig() {\n var list = aiMapper.selectList(null);\n AiEntity aiEntity = (list == null || list.isEmpty()) ? null : list.get(list.size() - 1);\n if (aiEntity == null) {\n aiEntity = createDefaultConfig();\n }\n return aiEntity;\n }\n\n /**\n * 获取所有AI配置\n */\n @Transactional(readOnly = true)\n public java.util.List<AiEntity> getAllAiConfigs() {\n return aiMapper.selectList(null);\n }\n\n /**\n * 根据ID获取AI配置\n */\n @Transactional(readOnly = true)\n public AiEntity getAiConfigById(Long id) {\n return aiMapper.selectById(id);\n }\n\n /**\n * 保存或更新AI配置(introduce/prompt)\n */\n @Transactional\n public AiEntity saveOrUpdateAiConfig(String introduce, String prompt) {\n var list = aiMapper.selectList(null);\n AiEntity aiEntity = (list == null || list.isEmpty()) ? null : list.get(list.size() - 1);\n\n if (aiEntity == null) {\n aiEntity = new AiEntity();\n aiEntity.setIntroduce(introduce);\n aiEntity.setPrompt(prompt);\n aiEntity.setCreatedAt(java.time.LocalDateTime.now());\n aiEntity.setUpdatedAt(java.time.LocalDateTime.now());\n aiMapper.insert(aiEntity);\n log.info(\"创建新的AI配置,ID: {}\", aiEntity.getId());\n } else {\n aiEntity.setIntroduce(introduce);\n aiEntity.setPrompt(prompt);\n aiEntity.setUpdatedAt(java.time.LocalDateTime.now());\n aiMapper.updateById(aiEntity);\n log.info(\"更新AI配置,ID: {}\", aiEntity.getId());\n }\n\n return aiEntity;\n }\n\n /**\n * 删除AI配置\n */\n @Transactional\n public boolean deleteAiConfig(Long id) {\n int result = aiMapper.deleteById(id);\n if (result > 0) {\n log.info(\"删除AI配置成功,ID: {}\", id);\n return true;\n }\n return false;\n }\n\n /**\n * 创建默认配置\n */\n @Transactional\n protected AiEntity createDefaultConfig() {\n AiEntity aiEntity = new AiEntity();\n aiEntity.setIntroduce(\"请在此填写您的技能介绍\");\n aiEntity.setPrompt(\"请在此填写AI提示词模板\");\n aiEntity.setCreatedAt(java.time.LocalDateTime.now());\n aiEntity.setUpdatedAt(java.time.LocalDateTime.now());\n aiMapper.insert(aiEntity);\n log.info(\"创建默认AI配置,ID: {}\", aiEntity.getId());\n return aiEntity;\n }\n\n /**\n * 测试AI连接(支持智谱和DeepSeek)\n * @return 测试结果\n */\n public String testDeepSeekConnection() {\n try {\n String testMessage = \"你好,请简单回复一个字:是\";\n String response = sendRequest(testMessage);\n return \"AI连接成功,响应: \" + response;\n } catch (Exception e) {\n log.error(\"AI连接测试失败\", e);\n return \"AI连接失败: \" + e.getMessage();\n }\n }\n\n /**\n * 获取当前使用的AI配置信息(用于调试)\n * @return 配置信息字符串\n */\n public String getCurrentAiConfig() {\n StringBuilder config = new StringBuilder();\n config.append(\"AI配置信息:\\n\");\n\n // 智谱AI环境变量配置\n String zhipuBaseUrl = System.getenv(\"ZHIPU_BASE_URL\");\n String zhipuApiKey = System.getenv(\"ZHIPU_API_KEY\");\n String zhipuModel = System.getenv(\"ZHIPU_MODEL\");\n\n if (zhipuBaseUrl != null || zhipuApiKey != null) {\n config.append(\"- 智谱AI环境变量配置:\\n\");\n config.append(\" ZHIPU_BASE_URL: \").append(zhipuBaseUrl != null ? zhipuBaseUrl : \"未设置\").append(\"\\n\");\n config.append(\" ZHIPU_API_KEY: \").append(zhipuApiKey != null ? maskApiKey(zhipuApiKey) : \"未设置\").append(\"\\n\");\n config.append(\" ZHIPU_MODEL: \").append(zhipuModel != null ? zhipuModel : \"未设置\").append(\"\\n\");\n }\n\n // DeepSeek环境变量配置\n String envBaseUrl = System.getenv(\"DEEPSEEK_BASE_URL\");\n String envApiKey = System.getenv(\"DEEPSEEK_API_KEY\");\n String envModel = System.getenv(\"DEEPSEEK_MODEL\");\n\n if (envBaseUrl != null || envApiKey != null) {\n config.append(\"- DeepSeek环境变量配置:\\n\");\n config.append(\" DEEPSEEK_BASE_URL: \").append(envBaseUrl != null ? envBaseUrl : \"未设置\").append(\"\\n\");\n config.append(\" DEEPSEEK_API_KEY: \").append(envApiKey != null ? maskApiKey(envApiKey) : \"未设置\").append(\"\\n\");\n config.append(\" DEEPSEEK_MODEL: \").append(envModel != null ? envModel : \"未设置\").append(\"\\n\");\n }\n\n // 数据库配置(回退选项)\n try {\n var cfg = configService.getAiConfigs();\n config.append(\"- 数据库配置:\\n\");\n config.append(\" BASE_URL: \").append(cfg.get(\"BASE_URL\")).append(\"\\n\");\n config.append(\" API_KEY: \").append(maskApiKey(cfg.get(\"API_KEY\"))).append(\"\\n\");\n config.append(\" MODEL: \").append(cfg.get(\"MODEL\")).append(\"\\n\");\n } catch (Exception e) {\n config.append(\"- 数据库配置: 读取失败\\n\");\n }\n\n return config.toString();\n }\n}"}, "8f8f72a71e82136e21c833cb56c253fb4ba15889": {"t": 1785755753.6926248, "n": 17098, "h": "1e0d909ea6d6e56a", "c": "package com.getjobs.application.service;\n\nimport com.getjobs.application.entity.AiEntity;\nimport com.getjobs.application.mapper.AiMapper;\nimport lombok.RequiredArgsConstructor;\nimport lombok.extern.slf4j.Slf4j;\nimport org.json.JSONArray;\nimport org.json.JSONObject;\nimport org.springframework.stereotype.Service;\nimport org.springframework.transaction.annotation.Transactional;\n\nimport java.net.URI;\nimport java.net.http.HttpClient;\nimport java.net.http.HttpRequest;\nimport java.net.http.HttpResponse;\nimport java.time.Duration;\nimport java.time.Instant;\nimport java.time.LocalDateTime;\nimport java.time.ZoneId;\nimport java.time.format.DateTimeFormatter;\n\n/**\n * AI 服务(Spring 管理)\n * 从数据库配置获取 BASE_URL、API_KEY、MODEL 并发起 AI 请求。\n */\n@Service\n@Slf4j\n@RequiredArgsConstructor\npublic class AiService {\n private final ConfigService configService;\n private final AiMapper aiMapper;\n\n /**\n * 隐藏API密钥,只显示前6位和后4位\n * @param apiKey 原始API密钥\n * @return 隐藏后的密钥字符串\n */\n private String maskApiKey(String apiKey) {\n if (apiKey == null || apiKey.length() < 8) {\n return \"INVALID_KEY\";\n }\n return apiKey.substring(0, 6) + \"********\" + apiKey.substring(apiKey.length() - 4);\n }\n\n /**\n * 发送 AI 请求(非流式)并返回回复内容。\n * @param content 用户消息内容\n * @return AI 回复文本\n */\n public String sendRequest(String content) {\n // 优先从环境变量读取智谱AI配置,如果没有则从数据库读取\n String baseUrl = System.getenv(\"ZHIPU_BASE_URL\");\n String apiKey = System.getenv(\"ZHIPU_API_KEY\");\n String model = System.getenv(\"ZHIPU_MODEL\");\n\n // 如果智谱环境变量不存在,尝试DeepSeek配置\n if (baseUrl == null || apiKey == null) {\n baseUrl = System.getenv(\"DEEPSEEK_BASE_URL\");\n apiKey = System.getenv(\"DEEPSEEK_API_KEY\");\n model = System.getenv(\"DEEPSEEK_MODEL\");\n }\n\n // 如果环境变量不存在,则回退到数据库配置\n if (baseUrl == null || apiKey == null) {\n var cfg = configService.getAiConfigs();\n baseUrl = baseUrl != null ? baseUrl : cfg.get(\"BASE_URL\");\n apiKey = apiKey != null ? apiKey : cfg.get(\"API_KEY\");\n model = model != null ? model : cfg.get(\"MODEL\");\n }\n\n // 如果仍然没有配置,使用智谱默认值\n if (baseUrl == null) baseUrl = \"https://open.bigmodel.cn/api/paas/v4\";\n if (model == null) model = \"glm-4\";\n\n // 校验必需参数\n if (apiKey == null || apiKey.trim().isEmpty()) {\n throw new RuntimeException(\"AI API Key 未配置,请设置 ZHIPU_API_KEY 或 DEEPSEEK_API_KEY 环境变量或数据库配置\");\n }\n\n log.info(\"使用AI配置 - BaseURL: {}, Model: {}, Key: {}...\",\n baseUrl, model, maskApiKey(apiKey));\n\n // 根据模型类型选择兼容的端点(部分“推理/Reasoning”模型需要使用 Responses API)\n String endpoint = isResponsesModel(model)\n ? buildResponsesEndpoint(baseUrl)\n : buildChatCompletionsEndpoint(baseUrl);\n\n int timeoutInSeconds = 60;\n\n HttpClient client = HttpClient.newBuilder()\n .connectTimeout(Duration.ofSeconds(timeoutInSeconds))\n .build();\n\n // 构建 JSON 请求体\n JSONObject requestData = new JSONObject();\n requestData.put(\"model\", model);\n requestData.put(\"temperature\", 0.5);\n if (endpoint.endsWith(\"/responses\")) {\n // Responses API 采用 input 字段\n requestData.put(\"input\", content);\n // 如需显式控制推理强度,可按需开启:\n // JSONObject reasoning = new JSONObject();\n // reasoning.put(\"effort\", \"medium\");\n // requestData.put(\"reasoning\", reasoning);\n } else {\n // Chat Completions API 使用 messages\n JSONArray messages = new JSONArray();\n JSONObject message = new JSONObject();\n message.put(\"role\", \"user\");\n message.put(\"content\", content);\n messages.put(message);\n requestData.put(\"messages\", messages);\n }\n\n HttpRequest request = HttpRequest.newBuilder()\n .uri(URI.create(endpoint))\n .header(\"Content-Type\", \"application/json\")\n .header(\"Accept\", \"application/json\")\n .header(\"Authorization\", \"Bearer \" + apiKey)\n // 某些服务(例如 Azure OpenAI)需要 api-key 头,额外加一层兼容\n .header(\"api-key\", apiKey)\n .POST(HttpRequest.BodyPublishers.ofString(requestData.toString()))\n .build();\n\n try {\n HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());\n if (response.statusCode() == 200) {\n JSONObject responseObject = new JSONObject(response.body());\n\n String requestId = responseObject.optString(\"id\");\n long created = responseObject.optLong(\"created\", 0);\n String usedModel = responseObject.optString(\"model\");\n\n String responseContent;\n if (endpoint.endsWith(\"/responses\")) {\n // Responses API:优先读取 output_text\n responseContent = responseObject.optString(\"output_text\", null);\n if (responseContent == null || responseContent.isEmpty()) {\n // 兜底:尝试从通用 choices/message 结构读取(部分代理/兼容层会返回该结构)\n try {\n JSONObject messageObject = responseObject.getJSONArray(\"choices\")\n .getJSONObject(0)\n .getJSONObject(\"message\");\n responseContent = messageObject.getString(\"content\");\n } catch (Exception ignore) {\n responseContent = response.body(); // 最后兜底:返回原始文本,避免空值\n }\n }\n } else {\n // Chat Completions API\n JSONObject messageObject = responseObject.getJSONArray(\"choices\")\n .getJSONObject(0)\n .getJSONObject(\"message\");\n responseContent = messageObject.getString(\"content\");\n }\n\n JSONObject usageObject = responseObject.optJSONObject(\"usage\");\n int promptTokens = usageObject != null ? usageObject.optInt(\"prompt_tokens\", -1) : -1;\n int completionTokens = usageObject != null ? usageObject.optInt(\"completion_tokens\", -1) : -1;\n int totalTokens = usageObject != null ? usageObject.optInt(\"total_tokens\", -1) : -1;\n\n LocalDateTime createdTime = created > 0\n ? Instant.ofEpochSecond(created).atZone(ZoneId.systemDefault()).toLocalDateTime()\n : LocalDateTime.now();\n DateTimeFormatter formatter = DateTimeFormatter.ofPattern(\"yyyy-MM-dd HH:mm:ss\");\n\n log.info(\"AI响应: id={}, time={}, model={}, promptTokens={}, completionTokens={}, totalTokens={}\",\n requestId, createdTime.format(formatter), usedModel, promptTokens, completionTokens, totalTokens);\n\n return responseContent;\n } else {\n // 更详细的错误日志,便于定位 400 问题\n log.error(\"AI请求失败: status={}, endpoint={}, body={}\", response.statusCode(), endpoint, response.body());\n // 针对 Responses-only 模型误用 Chat Completions 的常见错误做一次自动重试\n if (!endpoint.endsWith(\"/responses\") && containsReasoningParamError(response.body())) {\n String fallbackEndpoint = buildResponsesEndpoint(baseUrl);\n log.warn(\"检测到 reasoning 相关参数错误,自动切换到 Responses API 重试: {}\", fallbackEndpoint);\n return sendRequestViaResponses(content, apiKey, model, fallbackEndpoint);\n }\n throw new RuntimeException(\"AI请求失败,状态码: \" + response.statusCode() + \", 详情: \" + response.body());\n }\n } catch (Exception e) {\n log.error(\"调用AI服务异常\", e);\n throw e instanceof RuntimeException ? (RuntimeException) e : new RuntimeException(e);\n }\n }\n\n private String normalizeBaseUrl(String baseUrl) {\n if (baseUrl == null) return \"\";\n String trimmed = baseUrl.trim();\n return trimmed.endsWith(\"/\") ? trimmed.substring(0, trimmed.length() - 1) : trimmed;\n }\n\n /**\n * 根据配置构造 chat/completions 端点,避免重复拼接 /v1\n */\n private String buildChatCompletionsEndpoint(String baseUrl) {\n String normalized = normalizeBaseUrl(baseUrl);\n // 如果 baseUrl 已经包含 /v1(常见配置为 https://api.openai.com/v1),则只拼接 /chat/completions\n if (normalized.endsWith(\"/v1\") || normalized.contains(\"/v1/\")) {\n return normalized + \"/chat/completions\";\n }\n return normalized + \"/v1/chat/completions\";\n }\n\n /**\n * 构造 Responses API 端点\n */\n private String buildResponsesEndpoint(String baseUrl) {\n String normalized = normalizeBaseUrl(baseUrl);\n if (normalized.endsWith(\"/v1\") || normalized.contains(\"/v1/\")) {\n return normalized + \"/responses\";\n }\n return normalized + \"/v1/responses\";\n }\n\n /**\n * 粗略识别需要使用 Responses API 的模型(o-系列、4.1、reasoner 等)\n */\n private boolean isResponsesModel(String model) {\n if (model == null) return false;\n String m = model.toLowerCase();\n return m.contains(\"o1\") || m.contains(\"o3\") || m.contains(\"o4\")\n || m.contains(\"4.1\") || m.contains(\"reasoner\")\n || m.contains(\"4o-mini\") || m.contains(\"gpt-4o-mini\");\n }\n\n /**\n * 检查错误响应中是否包含 reasoning 相关参数错误(如 reasoning.summary unsupported_value)\n */\n private boolean containsReasoningParamError(String body) {\n if (body == null) return false;\n String s = body.toLowerCase();\n return (s.contains(\"reasoning\") && s.contains(\"unsupported_value\"))\n || s.contains(\"reasoning.summary\");\n }\n\n /**\n * 使用 Responses API 发送一次请求(用于自动降级/重试)\n */\n private String sendRequestViaResponses(String content, String apiKey, String model, String endpoint) {\n int timeoutInSeconds = 60;\n HttpClient client = HttpClient.newBuilder()\n .connectTimeout(Duration.ofSeconds(timeoutInSeconds))\n .build();\n\n JSONObject requestData = new JSONObject();\n requestData.put(\"model\", model);\n requestData.put(\"temperature\", 0.5);\n requestData.put(\"input\", content);\n\n HttpRequest request = HttpRequest.newBuilder()\n .uri(URI.create(endpoint))\n .header(\"Content-Type\", \"application/json\")\n .header(\"Accept\", \"application/json\")\n .header(\"Authorization\", \"Bearer \" + apiKey)\n .header(\"api-key\", apiKey)\n .POST(HttpRequest.BodyPublishers.ofString(requestData.toString()))\n .build();\n\n try {\n HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());\n if (response.statusCode() == 200) {\n JSONObject resp = new JSONObject(response.body());\n String outputText = resp.optString(\"output_text\", null);\n if (outputText != null && !outputText.isEmpty()) {\n return outputText;\n }\n // 兜底解析:部分兼容层可能返回 choices/message 结构\n try {\n JSONObject messageObject = resp.getJSONArray(\"choices\")\n .getJSONObject(0)\n .getJSONObject(\"message\");\n return messageObject.getString(\"content\");\n } catch (Exception ignore) {\n }\n // 无法解析则直接返回原始体,避免空值中断流程\n return response.body();\n }\n log.error(\"Responses API 调用失败: status={}, endpoint={}, body={}\", response.statusCode(), endpoint, response.body());\n throw new RuntimeException(\"AI请求失败,状态码: \" + response.statusCode() + \", 详情: \" + response.body());\n } catch (Exception e) {\n log.error(\"Responses API 调用异常\", e);\n throw e instanceof RuntimeException ? (RuntimeException) e : new RuntimeException(e);\n }\n }\n\n // ================= 合并的 AI 配置管理方法 =================\n\n /**\n * 获取AI配置(获取最新一条,如果不存在则创建默认配置)\n */\n @Transactional(readOnly = true)\n public AiEntity getAiConfig() {\n var list = aiMapper.selectList(null);\n AiEntity aiEntity = (list == null || list.isEmpty()) ? null : list.get(list.size() - 1);\n if (aiEntity == null) {\n aiEntity = createDefaultConfig();\n }\n return aiEntity;\n }\n\n /**\n * 获取所有AI配置\n */\n @Transactional(readOnly = true)\n public java.util.List<AiEntity> getAllAiConfigs() {\n return aiMapper.selectList(null);\n }\n\n /**\n * 根据ID获取AI配置\n */\n @Transactional(readOnly = true)\n public AiEntity getAiConfigById(Long id) {\n return aiMapper.selectById(id);\n }\n\n /**\n * 保存或更新AI配置(introduce/prompt)\n */\n @Transactional\n public AiEntity saveOrUpdateAiConfig(String introduce, String prompt) {\n var list = aiMapper.selectList(null);\n AiEntity aiEntity = (list == null || list.isEmpty()) ? null : list.get(list.size() - 1);\n\n if (aiEntity == null) {\n aiEntity = new AiEntity();\n aiEntity.setIntroduce(introduce);\n aiEntity.setPrompt(prompt);\n aiEntity.setCreatedAt(java.time.LocalDateTime.now());\n aiEntity.setUpdatedAt(java.time.LocalDateTime.now());\n aiMapper.insert(aiEntity);\n log.info(\"创建新的AI配置,ID: {}\", aiEntity.getId());\n } else {\n aiEntity.setIntroduce(introduce);\n aiEntity.setPrompt(prompt);\n aiEntity.setUpdatedAt(java.time.LocalDateTime.now());\n aiMapper.updateById(aiEntity);\n log.info(\"更新AI配置,ID: {}\", aiEntity.getId());\n }\n\n return aiEntity;\n }\n\n /**\n * 删除AI配置\n */\n @Transactional\n public boolean deleteAiConfig(Long id) {\n int result = aiMapper.deleteById(id);\n if (result > 0) {\n log.info(\"删除AI配置成功,ID: {}\", id);\n return true;\n }\n return false;\n }\n\n /**\n * 创建默认配置\n */\n @Transactional\n protected AiEntity createDefaultConfig() {\n AiEntity aiEntity = new AiEntity();\n aiEntity.setIntroduce(\"请在此填写您的技能介绍\");\n aiEntity.setPrompt(\"请在此填写AI提示词模板\");\n aiEntity.setCreatedAt(java.time.LocalDateTime.now());\n aiEntity.setUpdatedAt(java.time.LocalDateTime.now());\n aiMapper.insert(aiEntity);\n log.info(\"创建默认AI配置,ID: {}\", aiEntity.getId());\n return aiEntity;\n }\n\n /**\n * 测试AI连接(支持智谱和DeepSeek)\n * @return 测试结果\n */\n public String testDeepSeekConnection() {\n try {\n String testMessage = \"你好,请简单回复一个字:是\";\n String response = sendRequest(testMessage);\n return \"AI连接成功,响应: \" + response;\n } catch (Exception e) {\n log.error(\"AI连接测试失败\", e);\n return \"AI连接失败: \" + e.getMessage();\n }\n }\n\n /**\n * 获取当前使用的AI配置信息(用于调试)\n * @return 配置信息字符串\n */\n public String getCurrentAiConfig() {\n StringBuilder config = new StringBuilder();\n config.append(\"AI配置信息:\\n\");\n\n // 智谱AI环境变量配置\n String zhipuBaseUrl = System.getenv(\"ZHIPU_BASE_URL\");\n String zhipuApiKey = System.getenv(\"ZHIPU_API_KEY\");\n String zhipuModel = System.getenv(\"ZHIPU_MODEL\");\n\n if (zhipuBaseUrl != null || zhipuApiKey != null) {\n config.append(\"- 智谱AI环境变量配置:\\n\");\n config.append(\" ZHIPU_BASE_URL: \").append(zhipuBaseUrl != null ? zhipuBaseUrl : \"未设置\").append(\"\\n\");\n config.append(\" ZHIPU_API_KEY: \").append(zhipuApiKey != null ? maskApiKey(zhipuApiKey) : \"未设置\").append(\"\\n\");\n config.append(\" ZHIPU_MODEL: \").append(zhipuModel != null ? zhipuModel : \"未设置\").append(\"\\n\");\n }\n\n // DeepSeek环境变量配置\n String envBaseUrl = System.getenv(\"DEEPSEEK_BASE_URL\");\n String envApiKey = System.getenv(\"DEEPSEEK_API_KEY\");\n String envModel = System.getenv(\"DEEPSEEK_MODEL\");\n\n if (envBaseUrl != null || envApiKey != null) {\n config.append(\"- DeepSeek环境变量配置:\\n\");\n config.append(\" DEEPSEEK_BASE_URL: \").append(envBaseUrl != null ? envBaseUrl : \"未设置\").append(\"\\n\");\n config.append(\" DEEPSEEK_API_KEY: \").append(envApiKey != null ? maskApiKey(envApiKey) : \"未设置\").append(\"\\n\");\n config.append(\" DEEPSEEK_MODEL: \").append(envModel != null ? envModel : \"未设置\").append(\"\\n\");\n }\n\n // 数据库配置(回退选项)\n try {\n var cfg = configService.getAiConfigs();\n config.append(\"- 数据库配置:\\n\");\n config.append(\" BASE_URL: \").append(cfg.get(\"BASE_URL\")).append(\"\\n\");\n config.append(\" API_KEY: \").append(maskApiKey(cfg.get(\"API_KEY\"))).append(\"\\n\");\n config.append(\" MODEL: \").append(cfg.get(\"MODEL\")).append(\"\\n\");\n } catch (Exception e) {\n config.append(\"- 数据库配置: 读取失败\\n\");\n }\n\n return config.toString();\n }\n}"}, "1ea5bf33ef4afd4e7ad002d0356f61a42ee697a9": {"t": 1785755755.8944058, "n": 16156, "h": "6b996dfdc574a69f", "c": "package com.getjobs.application.service;\n\nimport com.getjobs.application.entity.AiEntity;\nimport com.getjobs.application.mapper.AiMapper;\nimport lombok.RequiredArgsConstructor;\nimport lombok.extern.slf4j.Slf4j;\nimport org.json.JSONArray;\nimport org.json.JSONObject;\nimport org.springframework.stereotype.Service;\nimport org.springframework.transaction.annotation.Transactional;\n\nimport java.net.URI;\nimport java.net.http.HttpClient;\nimport java.net.http.HttpRequest;\nimport java.net.http.HttpResponse;\nimport java.time.Duration;\nimport java.time.Instant;\nimport java.time.LocalDateTime;\nimport java.time.ZoneId;\nimport java.time.format.DateTimeFormatter;\n\n/**\n * AI 服务(Spring 管理)\n * 从数据库配置获取 BASE_URL、API_KEY、MODEL 并发起 AI 请求。\n */\n@Service\n@Slf4j\n@RequiredArgsConstructor\npublic class AiService {\n private final ConfigService configService;\n private final AiMapper aiMapper;\n\n /**\n * 隐藏API密钥,只显示前6位和后4位\n * @param apiKey 原始API密钥\n * @return 隐藏后的密钥字符串\n */\n private String maskApiKey(String apiKey) {\n if (apiKey == null || apiKey.length() < 8) {\n return \"INVALID_KEY\";\n }\n return apiKey.substring(0, 6) + \"********\" + apiKey.substring(apiKey.length() - 4);\n }\n\n /**\n * 发送 AI 请求(非流式)并返回回复内容。\n * @param content 用户消息内容\n * @return AI 回复文本\n */\n public String sendRequest(String content) {\n // 优先从环境变量读取 DeepSeek 配置,如果没有则从数据库读取\n String baseUrl = System.getenv(\"DEEPSEEK_BASE_URL\");\n String apiKey = System.getenv(\"DEEPSEEK_API_KEY\");\n String model = System.getenv(\"DEEPSEEK_MODEL\");\n\n // 如果环境变量不存在,则回退到数据库配置\n if (baseUrl == null || apiKey == null) {\n var cfg = configService.getAiConfigs();\n baseUrl = baseUrl != null ? baseUrl : cfg.get(\"BASE_URL\");\n apiKey = apiKey != null ? apiKey : cfg.get(\"API_KEY\");\n model = model != null ? model : cfg.get(\"MODEL\");\n }\n\n // 如果仍然没有配置,使用默认值\n if (baseUrl == null) baseUrl = \"https://api.deepseek.com\";\n if (model == null) model = \"deepseek-chat\";\n\n // 校验必需参数\n if (apiKey == null || apiKey.trim().isEmpty()) {\n throw new RuntimeException(\"AI API Key 未配置,请设置 DEEPSEEK_API_KEY 环境变量或数据库配置\");\n }\n\n log.info(\"使用AI配置 - BaseURL: {}, Model: {}, Key: {}...\",\n baseUrl, model, maskApiKey(apiKey));\n\n // 根据模型类型选择兼容的端点(部分“推理/Reasoning”模型需要使用 Responses API)\n String endpoint = isResponsesModel(model)\n ? buildResponsesEndpoint(baseUrl)\n : buildChatCompletionsEndpoint(baseUrl);\n\n int timeoutInSeconds = 60;\n\n HttpClient client = HttpClient.newBuilder()\n .connectTimeout(Duration.ofSeconds(timeoutInSeconds))\n .build();\n\n // 构建 JSON 请求体\n JSONObject requestData = new JSONObject();\n requestData.put(\"model\", model);\n requestData.put(\"temperature\", 0.5);\n if (endpoint.endsWith(\"/responses\")) {\n // Responses API 采用 input 字段\n requestData.put(\"input\", content);\n // 如需显式控制推理强度,可按需开启:\n // JSONObject reasoning = new JSONObject();\n // reasoning.put(\"effort\", \"medium\");\n // requestData.put(\"reasoning\", reasoning);\n } else {\n // Chat Completions API 使用 messages\n JSONArray messages = new JSONArray();\n JSONObject message = new JSONObject();\n message.put(\"role\", \"user\");\n message.put(\"content\", content);\n messages.put(message);\n requestData.put(\"messages\", messages);\n }\n\n HttpRequest request = HttpRequest.newBuilder()\n .uri(URI.create(endpoint))\n .header(\"Content-Type\", \"application/json\")\n .header(\"Accept\", \"application/json\")\n .header(\"Authorization\", \"Bearer \" + apiKey)\n // 某些服务(例如 Azure OpenAI)需要 api-key 头,额外加一层兼容\n .header(\"api-key\", apiKey)\n .POST(HttpRequest.BodyPublishers.ofString(requestData.toString()))\n .build();\n\n try {\n HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());\n if (response.statusCode() == 200) {\n JSONObject responseObject = new JSONObject(response.body());\n\n String requestId = responseObject.optString(\"id\");\n long created = responseObject.optLong(\"created\", 0);\n String usedModel = responseObject.optString(\"model\");\n\n String responseContent;\n if (endpoint.endsWith(\"/responses\")) {\n // Responses API:优先读取 output_text\n responseContent = responseObject.optString(\"output_text\", null);\n if (responseContent == null || responseContent.isEmpty()) {\n // 兜底:尝试从通用 choices/message 结构读取(部分代理/兼容层会返回该结构)\n try {\n JSONObject messageObject = responseObject.getJSONArray(\"choices\")\n .getJSONObject(0)\n .getJSONObject(\"message\");\n responseContent = messageObject.getString(\"content\");\n } catch (Exception ignore) {\n responseContent = response.body(); // 最后兜底:返回原始文本,避免空值\n }\n }\n } else {\n // Chat Completions API\n JSONObject messageObject = responseObject.getJSONArray(\"choices\")\n .getJSONObject(0)\n .getJSONObject(\"message\");\n responseContent = messageObject.getString(\"content\");\n }\n\n JSONObject usageObject = responseObject.optJSONObject(\"usage\");\n int promptTokens = usageObject != null ? usageObject.optInt(\"prompt_tokens\", -1) : -1;\n int completionTokens = usageObject != null ? usageObject.optInt(\"completion_tokens\", -1) : -1;\n int totalTokens = usageObject != null ? usageObject.optInt(\"total_tokens\", -1) : -1;\n\n LocalDateTime createdTime = created > 0\n ? Instant.ofEpochSecond(created).atZone(ZoneId.systemDefault()).toLocalDateTime()\n : LocalDateTime.now();\n DateTimeFormatter formatter = DateTimeFormatter.ofPattern(\"yyyy-MM-dd HH:mm:ss\");\n\n log.info(\"AI响应: id={}, time={}, model={}, promptTokens={}, completionTokens={}, totalTokens={}\",\n requestId, createdTime.format(formatter), usedModel, promptTokens, completionTokens, totalTokens);\n\n return responseContent;\n } else {\n // 更详细的错误日志,便于定位 400 问题\n log.error(\"AI请求失败: status={}, endpoint={}, body={}\", response.statusCode(), endpoint, response.body());\n // 针对 Responses-only 模型误用 Chat Completions 的常见错误做一次自动重试\n if (!endpoint.endsWith(\"/responses\") && containsReasoningParamError(response.body())) {\n String fallbackEndpoint = buildResponsesEndpoint(baseUrl);\n log.warn(\"检测到 reasoning 相关参数错误,自动切换到 Responses API 重试: {}\", fallbackEndpoint);\n return sendRequestViaResponses(content, apiKey, model, fallbackEndpoint);\n }\n throw new RuntimeException(\"AI请求失败,状态码: \" + response.statusCode() + \", 详情: \" + response.body());\n }\n } catch (Exception e) {\n log.error(\"调用AI服务异常\", e);\n throw e instanceof RuntimeException ? (RuntimeException) e : new RuntimeException(e);\n }\n }\n\n private String normalizeBaseUrl(String baseUrl) {\n if (baseUrl == null) return \"\";\n String trimmed = baseUrl.trim();\n return trimmed.endsWith(\"/\") ? trimmed.substring(0, trimmed.length() - 1) : trimmed;\n }\n\n /**\n * 根据配置构造 chat/completions 端点,避免重复拼接 /v1\n */\n private String buildChatCompletionsEndpoint(String baseUrl) {\n String normalized = normalizeBaseUrl(baseUrl);\n // 如果 baseUrl 已经包含 /v1(常见配置为 https://api.openai.com/v1),则只拼接 /chat/completions\n if (normalized.endsWith(\"/v1\") || normalized.contains(\"/v1/\")) {\n return normalized + \"/chat/completions\";\n }\n return normalized + \"/v1/chat/completions\";\n }\n\n /**\n * 构造 Responses API 端点\n */\n private String buildResponsesEndpoint(String baseUrl) {\n String normalized = normalizeBaseUrl(baseUrl);\n if (normalized.endsWith(\"/v1\") || normalized.contains(\"/v1/\")) {\n return normalized + \"/responses\";\n }\n return normalized + \"/v1/responses\";\n }\n\n /**\n * 粗略识别需要使用 Responses API 的模型(o-系列、4.1、reasoner 等)\n */\n private boolean isResponsesModel(String model) {\n if (model == null) return false;\n String m = model.toLowerCase();\n return m.contains(\"o1\") || m.contains(\"o3\") || m.contains(\"o4\")\n || m.contains(\"4.1\") || m.contains(\"reasoner\")\n || m.contains(\"4o-mini\") || m.contains(\"gpt-4o-mini\");\n }\n\n /**\n * 检查错误响应中是否包含 reasoning 相关参数错误(如 reasoning.summary unsupported_value)\n */\n private boolean containsReasoningParamError(String body) {\n if (body == null) return false;\n String s = body.toLowerCase();\n return (s.contains(\"reasoning\") && s.contains(\"unsupported_value\"))\n || s.contains(\"reasoning.summary\");\n }\n\n /**\n * 使用 Responses API 发送一次请求(用于自动降级/重试)\n */\n private String sendRequestViaResponses(String content, String apiKey, String model, String endpoint) {\n int timeoutInSeconds = 60;\n HttpClient client = HttpClient.newBuilder()\n .connectTimeout(Duration.ofSeconds(timeoutInSeconds))\n .build();\n\n JSONObject requestData = new JSONObject();\n requestData.put(\"model\", model);\n requestData.put(\"temperature\", 0.5);\n requestData.put(\"input\", content);\n\n HttpRequest request = HttpRequest.newBuilder()\n .uri(URI.create(endpoint))\n .header(\"Content-Type\", \"application/json\")\n .header(\"Accept\", \"application/json\")\n .header(\"Authorization\", \"Bearer \" + apiKey)\n .header(\"api-key\", apiKey)\n .POST(HttpRequest.BodyPublishers.ofString(requestData.toString()))\n .build();\n\n try {\n HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());\n if (response.statusCode() == 200) {\n JSONObject resp = new JSONObject(response.body());\n String outputText = resp.optString(\"output_text\", null);\n if (outputText != null && !outputText.isEmpty()) {\n return outputText;\n }\n // 兜底解析:部分兼容层可能返回 choices/message 结构\n try {\n JSONObject messageObject = resp.getJSONArray(\"choices\")\n .getJSONObject(0)\n .getJSONObject(\"message\");\n return messageObject.getString(\"content\");\n } catch (Exception ignore) {\n }\n // 无法解析则直接返回原始体,避免空值中断流程\n return response.body();\n }\n log.error(\"Responses API 调用失败: status={}, endpoint={}, body={}\", response.statusCode(), endpoint, response.body());\n throw new RuntimeException(\"AI请求失败,状态码: \" + response.statusCode() + \", 详情: \" + response.body());\n } catch (Exception e) {\n log.error(\"Responses API 调用异常\", e);\n throw e instanceof RuntimeException ? (RuntimeException) e : new RuntimeException(e);\n }\n }\n\n // ================= 合并的 AI 配置管理方法 =================\n\n /**\n * 获取AI配置(获取最新一条,如果不存在则创建默认配置)\n */\n @Transactional(readOnly = true)\n public AiEntity getAiConfig() {\n var list = aiMapper.selectList(null);\n AiEntity aiEntity = (list == null || list.isEmpty()) ? null : list.get(list.size() - 1);\n if (aiEntity == null) {\n aiEntity = createDefaultConfig();\n }\n return aiEntity;\n }\n\n /**\n * 获取所有AI配置\n */\n @Transactional(readOnly = true)\n public java.util.List<AiEntity> getAllAiConfigs() {\n return aiMapper.selectList(null);\n }\n\n /**\n * 根据ID获取AI配置\n */\n @Transactional(readOnly = true)\n public AiEntity getAiConfigById(Long id) {\n return aiMapper.selectById(id);\n }\n\n /**\n * 保存或更新AI配置(introduce/prompt)\n */\n @Transactional\n public AiEntity saveOrUpdateAiConfig(String introduce, String prompt) {\n var list = aiMapper.selectList(null);\n AiEntity aiEntity = (list == null || list.isEmpty()) ? null : list.get(list.size() - 1);\n\n if (aiEntity == null) {\n aiEntity = new AiEntity();\n aiEntity.setIntroduce(introduce);\n aiEntity.setPrompt(prompt);\n aiEntity.setCreatedAt(java.time.LocalDateTime.now());\n aiEntity.setUpdatedAt(java.time.LocalDateTime.now());\n aiMapper.insert(aiEntity);\n log.info(\"创建新的AI配置,ID: {}\", aiEntity.getId());\n } else {\n aiEntity.setIntroduce(introduce);\n aiEntity.setPrompt(prompt);\n aiEntity.setUpdatedAt(java.time.LocalDateTime.now());\n aiMapper.updateById(aiEntity);\n log.info(\"更新AI配置,ID: {}\", aiEntity.getId());\n }\n\n return aiEntity;\n }\n\n /**\n * 删除AI配置\n */\n @Transactional\n public boolean deleteAiConfig(Long id) {\n int result = aiMapper.deleteById(id);\n if (result > 0) {\n log.info(\"删除AI配置成功,ID: {}\", id);\n return true;\n }\n return false;\n }\n\n /**\n * 创建默认配置\n */\n @Transactional\n protected AiEntity createDefaultConfig() {\n AiEntity aiEntity = new AiEntity();\n aiEntity.setIntroduce(\"请在此填写您的技能介绍\");\n aiEntity.setPrompt(\"请在此填写AI提示词模板\");\n aiEntity.setCreatedAt(java.time.LocalDateTime.now());\n aiEntity.setUpdatedAt(java.time.LocalDateTime.now());\n aiMapper.insert(aiEntity);\n log.info(\"创建默认AI配置,ID: {}\", aiEntity.getId());\n return aiEntity;\n }\n\n /**\n * 测试DeepSeek连接\n * @return 测试结果\n */\n public String testDeepSeekConnection() {\n try {\n String testMessage = \"你好,请简单回复一个字:是\";\n String response = sendRequest(testMessage);\n return \"DeepSeek连接成功,响应: \" + response;\n } catch (Exception e) {\n log.error(\"DeepSeek连接测试失败\", e);\n return \"DeepSeek连接失败: \" + e.getMessage();\n }\n }\n\n /**\n * 获取当前使用的AI配置信息(用于调试)\n * @return 配置信息字符串\n */\n public String getCurrentAiConfig() {\n StringBuilder config = new StringBuilder();\n config.append(\"AI配置信息:\\n\");\n\n // 环境变量配置\n String envBaseUrl = System.getenv(\"DEEPSEEK_BASE_URL\");\n String envApiKey = System.getenv(\"DEEPSEEK_API_KEY\");\n String envModel = System.getenv(\"DEEPSEEK_MODEL\");\n\n if (envBaseUrl != null || envApiKey != null) {\n config.append(\"- 环境变量配置:\\n\");\n config.append(\" DEEPSEEK_BASE_URL: \").append(envBaseUrl != null ? envBaseUrl : \"未设置\").append(\"\\n\");\n config.append(\" DEEPSEEK_API_KEY: \").append(envApiKey != null ? maskApiKey(envApiKey) : \"未设置\").append(\"\\n\");\n config.append(\" DEEPSEEK_MODEL: \").append(envModel != null ? envModel : \"未设置\").append(\"\\n\");\n }\n\n // 数据库配置(回退选项)\n try {\n var cfg = configService.getAiConfigs();\n config.append(\"- 数据库配置:\\n\");\n config.append(\" BASE_URL: \").append(cfg.get(\"BASE_URL\")).append(\"\\n\");\n config.append(\" API_KEY: \").append(maskApiKey(cfg.get(\"API_KEY\"))).append(\"\\n\");\n config.append(\" MODEL: \").append(cfg.get(\"MODEL\")).append(\"\\n\");\n } catch (Exception e) {\n config.append(\"- 数据库配置: 读取失败\\n\");\n }\n\n return config.toString();\n }\n}"}, "5f7e15f4c80f4ec7f35ca018e584880a37d62138": {"t": 1785755757.4814968, "n": 13226, "h": "f983ce2b295343c8", "c": "package com.getjobs.application.service;\n\nimport com.getjobs.application.entity.AiEntity;\nimport com.getjobs.application.mapper.AiMapper;\nimport lombok.RequiredArgsConstructor;\nimport lombok.extern.slf4j.Slf4j;\nimport org.json.JSONArray;\nimport org.json.JSONObject;\nimport org.springframework.stereotype.Service;\nimport org.springframework.transaction.annotation.Transactional;\n\nimport java.net.URI;\nimport java.net.http.HttpClient;\nimport java.net.http.HttpRequest;\nimport java.net.http.HttpResponse;\nimport java.time.Duration;\nimport java.time.Instant;\nimport java.time.LocalDateTime;\nimport java.time.ZoneId;\nimport java.time.format.DateTimeFormatter;\n\n/**\n * AI 服务(Spring 管理)\n * 从数据库配置获取 BASE_URL、API_KEY、MODEL 并发起 AI 请求。\n */\n@Service\n@Slf4j\n@RequiredArgsConstructor\npublic class AiService {\n private final ConfigService configService;\n private final AiMapper aiMapper;\n\n /**\n * 发送 AI 请求(非流式)并返回回复内容。\n * @param content 用户消息内容\n * @return AI 回复文本\n */\n public String sendRequest(String content) {\n // 读取并校验配置\n var cfg = configService.getAiConfigs();\n String baseUrl = cfg.get(\"BASE_URL\");\n String apiKey = cfg.get(\"API_KEY\");\n String model = cfg.get(\"MODEL\");\n // 根据模型类型选择兼容的端点(部分“推理/Reasoning”模型需要使用 Responses API)\n String endpoint = isResponsesModel(model)\n ? buildResponsesEndpoint(baseUrl)\n : buildChatCompletionsEndpoint(baseUrl);\n\n int timeoutInSeconds = 60;\n\n HttpClient client = HttpClient.newBuilder()\n .connectTimeout(Duration.ofSeconds(timeoutInSeconds))\n .build();\n\n // 构建 JSON 请求体\n JSONObject requestData = new JSONObject();\n requestData.put(\"model\", model);\n requestData.put(\"temperature\", 0.5);\n if (endpoint.endsWith(\"/responses\")) {\n // Responses API 采用 input 字段\n requestData.put(\"input\", content);\n // 如需显式控制推理强度,可按需开启:\n // JSONObject reasoning = new JSONObject();\n // reasoning.put(\"effort\", \"medium\");\n // requestData.put(\"reasoning\", reasoning);\n } else {\n // Chat Completions API 使用 messages\n JSONArray messages = new JSONArray();\n JSONObject message = new JSONObject();\n message.put(\"role\", \"user\");\n message.put(\"content\", content);\n messages.put(message);\n requestData.put(\"messages\", messages);\n }\n\n HttpRequest request = HttpRequest.newBuilder()\n .uri(URI.create(endpoint))\n .header(\"Content-Type\", \"application/json\")\n .header(\"Accept\", \"application/json\")\n .header(\"Authorization\", \"Bearer \" + apiKey)\n // 某些服务(例如 Azure OpenAI)需要 api-key 头,额外加一层兼容\n .header(\"api-key\", apiKey)\n .POST(HttpRequest.BodyPublishers.ofString(requestData.toString()))\n .build();\n\n try {\n HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());\n if (response.statusCode() == 200) {\n JSONObject responseObject = new JSONObject(response.body());\n\n String requestId = responseObject.optString(\"id\");\n long created = responseObject.optLong(\"created\", 0);\n String usedModel = responseObject.optString(\"model\");\n\n String responseContent;\n if (endpoint.endsWith(\"/responses\")) {\n // Responses API:优先读取 output_text\n responseContent = responseObject.optString(\"output_text\", null);\n if (responseContent == null || responseContent.isEmpty()) {\n // 兜底:尝试从通用 choices/message 结构读取(部分代理/兼容层会返回该结构)\n try {\n JSONObject messageObject = responseObject.getJSONArray(\"choices\")\n .getJSONObject(0)\n .getJSONObject(\"message\");\n responseContent = messageObject.getString(\"content\");\n } catch (Exception ignore) {\n responseContent = response.body(); // 最后兜底:返回原始文本,避免空值\n }\n }\n } else {\n // Chat Completions API\n JSONObject messageObject = responseObject.getJSONArray(\"choices\")\n .getJSONObject(0)\n .getJSONObject(\"message\");\n responseContent = messageObject.getString(\"content\");\n }\n\n JSONObject usageObject = responseObject.optJSONObject(\"usage\");\n int promptTokens = usageObject != null ? usageObject.optInt(\"prompt_tokens\", -1) : -1;\n int completionTokens = usageObject != null ? usageObject.optInt(\"completion_tokens\", -1) : -1;\n int totalTokens = usageObject != null ? usageObject.optInt(\"total_tokens\", -1) : -1;\n\n LocalDateTime createdTime = created > 0\n ? Instant.ofEpochSecond(created).atZone(ZoneId.systemDefault()).toLocalDateTime()\n : LocalDateTime.now();\n DateTimeFormatter formatter = DateTimeFormatter.ofPattern(\"yyyy-MM-dd HH:mm:ss\");\n\n log.info(\"AI响应: id={}, time={}, model={}, promptTokens={}, completionTokens={}, totalTokens={}\",\n requestId, createdTime.format(formatter), usedModel, promptTokens, completionTokens, totalTokens);\n\n return responseContent;\n } else {\n // 更详细的错误日志,便于定位 400 问题\n log.error(\"AI请求失败: status={}, endpoint={}, body={}\", response.statusCode(), endpoint, response.body());\n // 针对 Responses-only 模型误用 Chat Completions 的常见错误做一次自动重试\n if (!endpoint.endsWith(\"/responses\") && containsReasoningParamError(response.body())) {\n String fallbackEndpoint = buildResponsesEndpoint(baseUrl);\n log.warn(\"检测到 reasoning 相关参数错误,自动切换到 Responses API 重试: {}\", fallbackEndpoint);\n return sendRequestViaResponses(content, apiKey, model, fallbackEndpoint);\n }\n throw new RuntimeException(\"AI请求失败,状态码: \" + response.statusCode() + \", 详情: \" + response.body());\n }\n } catch (Exception e) {\n log.error(\"调用AI服务异常\", e);\n throw e instanceof RuntimeException ? (RuntimeException) e : new RuntimeException(e);\n }\n }\n\n private String normalizeBaseUrl(String baseUrl) {\n if (baseUrl == null) return \"\";\n String trimmed = baseUrl.trim();\n return trimmed.endsWith(\"/\") ? trimmed.substring(0, trimmed.length() - 1) : trimmed;\n }\n\n /**\n * 根据配置构造 chat/completions 端点,避免重复拼接 /v1\n */\n private String buildChatCompletionsEndpoint(String baseUrl) {\n String normalized = normalizeBaseUrl(baseUrl);\n // 如果 baseUrl 已经包含 /v1(常见配置为 https://api.openai.com/v1),则只拼接 /chat/completions\n if (normalized.endsWith(\"/v1\") || normalized.contains(\"/v1/\")) {\n return normalized + \"/chat/completions\";\n }\n return normalized + \"/v1/chat/completions\";\n }\n\n /**\n * 构造 Responses API 端点\n */\n private String buildResponsesEndpoint(String baseUrl) {\n String normalized = normalizeBaseUrl(baseUrl);\n if (normalized.endsWith(\"/v1\") || normalized.contains(\"/v1/\")) {\n return normalized + \"/responses\";\n }\n return normalized + \"/v1/responses\";\n }\n\n /**\n * 粗略识别需要使用 Responses API 的模型(o-系列、4.1、reasoner 等)\n */\n private boolean isResponsesModel(String model) {\n if (model == null) return false;\n String m = model.toLowerCase();\n return m.contains(\"o1\") || m.contains(\"o3\") || m.contains(\"o4\")\n || m.contains(\"4.1\") || m.contains(\"reasoner\")\n || m.contains(\"4o-mini\") || m.contains(\"gpt-4o-mini\");\n }\n\n /**\n * 检查错误响应中是否包含 reasoning 相关参数错误(如 reasoning.summary unsupported_value)\n */\n private boolean containsReasoningParamError(String body) {\n if (body == null) return false;\n String s = body.toLowerCase();\n return (s.contains(\"reasoning\") && s.contains(\"unsupported_value\"))\n || s.contains(\"reasoning.summary\");\n }\n\n /**\n * 使用 Responses API 发送一次请求(用于自动降级/重试)\n */\n private String sendRequestViaResponses(String content, String apiKey, String model, String endpoint) {\n int timeoutInSeconds = 60;\n HttpClient client = HttpClient.newBuilder()\n .connectTimeout(Duration.ofSeconds(timeoutInSeconds))\n .build();\n\n JSONObject requestData = new JSONObject();\n requestData.put(\"model\", model);\n requestData.put(\"temperature\", 0.5);\n requestData.put(\"input\", content);\n\n HttpRequest request = HttpRequest.newBuilder()\n .uri(URI.create(endpoint))\n .header(\"Content-Type\", \"application/json\")\n .header(\"Accept\", \"application/json\")\n .header(\"Authorization\", \"Bearer \" + apiKey)\n .header(\"api-key\", apiKey)\n .POST(HttpRequest.BodyPublishers.ofString(requestData.toString()))\n .build();\n\n try {\n HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());\n if (response.statusCode() == 200) {\n JSONObject resp = new JSONObject(response.body());\n String outputText = resp.optString(\"output_text\", null);\n if (outputText != null && !outputText.isEmpty()) {\n return outputText;\n }\n // 兜底解析:部分兼容层可能返回 choices/message 结构\n try {\n JSONObject messageObject = resp.getJSONArray(\"choices\")\n .getJSONObject(0)\n .getJSONObject(\"message\");\n return messageObject.getString(\"content\");\n } catch (Exception ignore) {\n }\n // 无法解析则直接返回原始体,避免空值中断流程\n return response.body();\n }\n log.error(\"Responses API 调用失败: status={}, endpoint={}, body={}\", response.statusCode(), endpoint, response.body());\n throw new RuntimeException(\"AI请求失败,状态码: \" + response.statusCode() + \", 详情: \" + response.body());\n } catch (Exception e) {\n log.error(\"Responses API 调用异常\", e);\n throw e instanceof RuntimeException ? (RuntimeException) e : new RuntimeException(e);\n }\n }\n\n // ================= 合并的 AI 配置管理方法 =================\n\n /**\n * 获取AI配置(获取最新一条,如果不存在则创建默认配置)\n */\n @Transactional(readOnly = true)\n public AiEntity getAiConfig() {\n var list = aiMapper.selectList(null);\n AiEntity aiEntity = (list == null || list.isEmpty()) ? null : list.get(list.size() - 1);\n if (aiEntity == null) {\n aiEntity = createDefaultConfig();\n }\n return aiEntity;\n }\n\n /**\n * 获取所有AI配置\n */\n @Transactional(readOnly = true)\n public java.util.List<AiEntity> getAllAiConfigs() {\n return aiMapper.selectList(null);\n }\n\n /**\n * 根据ID获取AI配置\n */\n @Transactional(readOnly = true)\n public AiEntity getAiConfigById(Long id) {\n return aiMapper.selectById(id);\n }\n\n /**\n * 保存或更新AI配置(introduce/prompt)\n */\n @Transactional\n public AiEntity saveOrUpdateAiConfig(String introduce, String prompt) {\n var list = aiMapper.selectList(null);\n AiEntity aiEntity = (list == null || list.isEmpty()) ? null : list.get(list.size() - 1);\n\n if (aiEntity == null) {\n aiEntity = new AiEntity();\n aiEntity.setIntroduce(introduce);\n aiEntity.setPrompt(prompt);\n aiEntity.setCreatedAt(java.time.LocalDateTime.now());\n aiEntity.setUpdatedAt(java.time.LocalDateTime.now());\n aiMapper.insert(aiEntity);\n log.info(\"创建新的AI配置,ID: {}\", aiEntity.getId());\n } else {\n aiEntity.setIntroduce(introduce);\n aiEntity.setPrompt(prompt);\n aiEntity.setUpdatedAt(java.time.LocalDateTime.now());\n aiMapper.updateById(aiEntity);\n log.info(\"更新AI配置,ID: {}\", aiEntity.getId());\n }\n\n return aiEntity;\n }\n\n /**\n * 删除AI配置\n */\n @Transactional\n public boolean deleteAiConfig(Long id) {\n int result = aiMapper.deleteById(id);\n if (result > 0) {\n log.info(\"删除AI配置成功,ID: {}\", id);\n return true;\n }\n return false;\n }\n\n /**\n * 创建默认配置\n */\n @Transactional\n protected AiEntity createDefaultConfig() {\n AiEntity aiEntity = new AiEntity();\n aiEntity.setIntroduce(\"请在此填写您的技能介绍\");\n aiEntity.setPrompt(\"请在此填写AI提示词模板\");\n aiEntity.setCreatedAt(java.time.LocalDateTime.now());\n aiEntity.setUpdatedAt(java.time.LocalDateTime.now());\n aiMapper.insert(aiEntity);\n log.info(\"创建默认AI配置,ID: {}\", aiEntity.getId());\n return aiEntity;\n }\n}"}, "bb5fb910756f32dc75328a3ff449fb9378e99c9c": {"t": 1785755758.8899488, "n": 7821, "h": "510a9cb3968e1382", "c": "package com.getjobs.application.service;\n\nimport com.getjobs.application.entity.AiEntity;\nimport com.getjobs.application.mapper.AiMapper;\nimport lombok.Setter;\nimport lombok.extern.slf4j.Slf4j;\nimport org.json.JSONArray;\nimport org.json.JSONObject;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.stereotype.Service;\nimport org.springframework.transaction.annotation.Transactional;\n\nimport java.net.URI;\nimport java.net.http.HttpClient;\nimport java.net.http.HttpRequest;\nimport java.net.http.HttpResponse;\nimport java.time.Duration;\nimport java.time.Instant;\nimport java.time.LocalDateTime;\nimport java.time.ZoneId;\nimport java.time.format.DateTimeFormatter;\n\n/**\n * AI 服务(Spring 管理)\n * 从数据库配置获取 BASE_URL、API_KEY、MODEL 并发起 AI 请求。\n */\n@Setter\n@Service\n@Slf4j\npublic class AiService {\n\n @Autowired\n private ConfigService configService;\n\n @Autowired\n private AiMapper aiMapper;\n\n /**\n * 发送 AI 请求(非流式)并返回回复内容。\n * @param content 用户消息内容\n * @return AI 回复文本\n */\n public String sendRequest(String content) {\n // 读取并校验配置\n var cfg = configService.getAiConfigs();\n String baseUrl = cfg.get(\"BASE_URL\");\n String apiKey = cfg.get(\"API_KEY\");\n String model = cfg.get(\"MODEL\");\n\n String endpoint = buildChatCompletionsEndpoint(baseUrl);\n\n int timeoutInSeconds = 60;\n\n HttpClient client = HttpClient.newBuilder()\n .connectTimeout(Duration.ofSeconds(timeoutInSeconds))\n .build();\n\n // 构建 JSON 请求体\n JSONObject requestData = new JSONObject();\n requestData.put(\"model\", model);\n requestData.put(\"temperature\", 0.5);\n\n JSONArray messages = new JSONArray();\n JSONObject message = new JSONObject();\n message.put(\"role\", \"user\");\n message.put(\"content\", content);\n messages.put(message);\n requestData.put(\"messages\", messages);\n\n HttpRequest request = HttpRequest.newBuilder()\n .uri(URI.create(endpoint))\n .header(\"Content-Type\", \"application/json\")\n .header(\"Accept\", \"application/json\")\n .header(\"Authorization\", \"Bearer \" + apiKey)\n // 某些服务(例如 Azure OpenAI)需要 api-key 头,额外加一层兼容\n .header(\"api-key\", apiKey)\n .POST(HttpRequest.BodyPublishers.ofString(requestData.toString()))\n .build();\n\n try {\n HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());\n if (response.statusCode() == 200) {\n JSONObject responseObject = new JSONObject(response.body());\n\n String requestId = responseObject.optString(\"id\");\n long created = responseObject.optLong(\"created\", 0);\n String usedModel = responseObject.optString(\"model\");\n\n JSONObject messageObject = responseObject.getJSONArray(\"choices\")\n .getJSONObject(0)\n .getJSONObject(\"message\");\n String responseContent = messageObject.getString(\"content\");\n\n JSONObject usageObject = responseObject.optJSONObject(\"usage\");\n int promptTokens = usageObject != null ? usageObject.optInt(\"prompt_tokens\", -1) : -1;\n int completionTokens = usageObject != null ? usageObject.optInt(\"completion_tokens\", -1) : -1;\n int totalTokens = usageObject != null ? usageObject.optInt(\"total_tokens\", -1) : -1;\n\n LocalDateTime createdTime = created > 0\n ? Instant.ofEpochSecond(created).atZone(ZoneId.systemDefault()).toLocalDateTime()\n : LocalDateTime.now();\n DateTimeFormatter formatter = DateTimeFormatter.ofPattern(\"yyyy-MM-dd HH:mm:ss\");\n\n log.info(\"AI响应: id={}, time={}, model={}, promptTokens={}, completionTokens={}, totalTokens={}\",\n requestId, createdTime.format(formatter), usedModel, promptTokens, completionTokens, totalTokens);\n\n return responseContent;\n } else {\n // 更详细的错误日志,便于定位 400 问题\n log.error(\"AI请求失败: status={}, endpoint={}, body={}\", response.statusCode(), endpoint, response.body());\n throw new RuntimeException(\"AI请求失败,状态码: \" + response.statusCode() + \", 详情: \" + response.body());\n }\n } catch (Exception e) {\n log.error(\"调用AI服务异常\", e);\n throw e instanceof RuntimeException ? (RuntimeException) e : new RuntimeException(e);\n }\n }\n\n private String normalizeBaseUrl(String baseUrl) {\n if (baseUrl == null) return \"\";\n String trimmed = baseUrl.trim();\n return trimmed.endsWith(\"/\") ? trimmed.substring(0, trimmed.length() - 1) : trimmed;\n }\n\n /**\n * 根据配置构造 chat/completions 端点,避免重复拼接 /v1\n */\n private String buildChatCompletionsEndpoint(String baseUrl) {\n String normalized = normalizeBaseUrl(baseUrl);\n // 如果 baseUrl 已经包含 /v1(常见配置为 https://api.openai.com/v1),则只拼接 /chat/completions\n if (normalized.endsWith(\"/v1\") || normalized.contains(\"/v1/\")) {\n return normalized + \"/chat/completions\";\n }\n return normalized + \"/v1/chat/completions\";\n }\n\n // ================= 合并的 AI 配置管理方法 =================\n\n /**\n * 获取AI配置(获取最新一条,如果不存在则创建默认配置)\n */\n @Transactional(readOnly = true)\n public AiEntity getAiConfig() {\n var list = aiMapper.selectList(null);\n AiEntity aiEntity = (list == null || list.isEmpty()) ? null : list.get(list.size() - 1);\n if (aiEntity == null) {\n aiEntity = createDefaultConfig();\n }\n return aiEntity;\n }\n\n /**\n * 获取所有AI配置\n */\n @Transactional(readOnly = true)\n public java.util.List<AiEntity> getAllAiConfigs() {\n return aiMapper.selectList(null);\n }\n\n /**\n * 根据ID获取AI配置\n */\n @Transactional(readOnly = true)\n public AiEntity getAiConfigById(Long id) {\n return aiMapper.selectById(id);\n }\n\n /**\n * 保存或更新AI配置(introduce/prompt)\n */\n @Transactional\n public AiEntity saveOrUpdateAiConfig(String introduce, String prompt) {\n var list = aiMapper.selectList(null);\n AiEntity aiEntity = (list == null || list.isEmpty()) ? null : list.get(list.size() - 1);\n\n if (aiEntity == null) {\n aiEntity = new AiEntity();\n aiEntity.setIntroduce(introduce);\n aiEntity.setPrompt(prompt);\n aiEntity.setCreatedAt(java.time.LocalDateTime.now());\n aiEntity.setUpdatedAt(java.time.LocalDateTime.now());\n aiMapper.insert(aiEntity);\n log.info(\"创建新的AI配置,ID: {}\", aiEntity.getId());\n } else {\n aiEntity.setIntroduce(introduce);\n aiEntity.setPrompt(prompt);\n aiEntity.setUpdatedAt(java.time.LocalDateTime.now());\n aiMapper.updateById(aiEntity);\n log.info(\"更新AI配置,ID: {}\", aiEntity.getId());\n }\n\n return aiEntity;\n }\n\n /**\n * 删除AI配置\n */\n @Transactional\n public boolean deleteAiConfig(Long id) {\n int result = aiMapper.deleteById(id);\n if (result > 0) {\n log.info(\"删除AI配置成功,ID: {}\", id);\n return true;\n }\n return false;\n }\n\n /**\n * 创建默认配置\n */\n @Transactional\n protected AiEntity createDefaultConfig() {\n AiEntity aiEntity = new AiEntity();\n aiEntity.setIntroduce(\"请在此填写您的技能介绍\");\n aiEntity.setPrompt(\"请在此填写AI提示词模板\");\n aiEntity.setCreatedAt(java.time.LocalDateTime.now());\n aiEntity.setUpdatedAt(java.time.LocalDateTime.now());\n aiMapper.insert(aiEntity);\n log.info(\"创建默认AI配置,ID: {}\", aiEntity.getId());\n return aiEntity;\n }\n}"}}, "src/main/java/com/getjobs/application/controller/ConfigController.java": {"f15092fe12e70daeaca24e07572494639513958c": {"t": 1785746115.7300723, "n": 6917, "h": "3871eced2e2d473e", "c": "package com.getjobs.application.controller;\n\nimport com.getjobs.application.service.ConfigService;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.http.ResponseEntity;\nimport org.springframework.web.bind.annotation.*;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\n/**\n * 配置控制器\n * 提供配置管理的REST API接口\n */\n@Slf4j\n@RestController\n@RequestMapping(\"/api/config\")\n@CrossOrigin(origins = \"*\")\npublic class ConfigController {\n\n @Autowired\n private ConfigService configService;\n\n /**\n * 获取所有配置\n * @return 配置Map\n */\n @GetMapping\n public ResponseEntity<Map<String, Object>> getAllConfigs() {\n Map<String, Object> response = new HashMap<>();\n\n try {\n Map<String, String> configs = configService.getAllConfigsAsMap();\n\n response.put(\"success\", true);\n response.put(\"data\", configs);\n response.put(\"message\", \"获取配置成功\");\n\n return ResponseEntity.ok(response);\n\n } catch (Exception e) {\n log.error(\"获取配置失败\", e);\n response.put(\"success\", false);\n response.put(\"message\", \"获取配置失败: \" + e.getMessage());\n return ResponseEntity.internalServerError().body(response);\n }\n }\n\n /**\n * 根据配置键获取单个配置\n * @param key 配置键\n * @return 配置值\n */\n @GetMapping(\"/{key}\")\n public ResponseEntity<Map<String, Object>> getConfigByKey(@PathVariable String key) {\n Map<String, Object> response = new HashMap<>();\n\n try {\n var config = configService.getConfigByKey(key);\n\n if (config != null) {\n response.put(\"success\", true);\n response.put(\"data\", config);\n response.put(\"message\", \"获取配置成功\");\n return ResponseEntity.ok(response);\n } else {\n response.put(\"success\", false);\n response.put(\"message\", \"配置不存在\");\n return ResponseEntity.notFound().build();\n }\n\n } catch (Exception e) {\n log.error(\"获取配置失败: {}\", key, e);\n response.put(\"success\", false);\n response.put(\"message\", \"获取配置失败: \" + e.getMessage());\n return ResponseEntity.internalServerError().body(response);\n }\n }\n\n /**\n * 批量更新配置\n * @param configMap 配置Map,key为config_key,value为config_value\n * @return 更新结果\n */\n @PostMapping\n public ResponseEntity<Map<String, Object>> batchUpdateConfigs(@RequestBody Map<String, String> configMap) {\n Map<String, Object> response = new HashMap<>();\n\n try {\n if (configMap == null || configMap.isEmpty()) {\n response.put(\"success\", false);\n response.put(\"message\", \"配置数据不能为空\");\n return ResponseEntity.badRequest().body(response);\n }\n\n int updateCount = configService.batchUpdateConfigs(configMap);\n\n response.put(\"success\", true);\n response.put(\"message\", \"配置更新成功\");\n response.put(\"updateCount\", updateCount);\n\n log.info(\"批量更新配置成功,共更新 {} 项\", updateCount);\n return ResponseEntity.ok(response);\n\n } catch (Exception e) {\n log.error(\"批量更新配置失败\", e);\n response.put(\"success\", false);\n response.put(\"message\", \"配置更新失败: \" + e.getMessage());\n return ResponseEntity.internalServerError().body(response);\n }\n }\n\n /**\n * 更新单个配置\n * @param key 配置键\n * @param requestBody 请求体包含value\n * @return 更新结果\n */\n @PutMapping(\"/{key}\")\n public ResponseEntity<Map<String, Object>> updateConfig(\n @PathVariable String key,\n @RequestBody Map<String, String> requestBody) {\n\n Map<String, Object> response = new HashMap<>();\n\n try {\n String value = requestBody.get(\"value\");\n\n if (value == null) {\n response.put(\"success\", false);\n response.put(\"message\", \"配置值不能为空\");\n return ResponseEntity.badRequest().body(response);\n }\n\n boolean success = configService.updateConfig(key, value);\n\n if (success) {\n response.put(\"success\", true);\n response.put(\"message\", \"配置更新成功\");\n return ResponseEntity.ok(response);\n } else {\n response.put(\"success\", false);\n response.put(\"message\", \"配置更新失败,配置键可能不存在\");\n return ResponseEntity.badRequest().body(response);\n }\n\n } catch (Exception e) {\n log.error(\"更新配置失败: {}\", key, e);\n response.put(\"success\", false);\n response.put(\"message\", \"配置更新失败: \" + e.getMessage());\n return ResponseEntity.internalServerError().body(response);\n }\n }\n\n /**\n * 健康检查接口\n * @return 服务状态\n */\n @GetMapping(\"/health\")\n public ResponseEntity<Map<String, Object>> healthCheck() {\n Map<String, Object> response = new HashMap<>();\n response.put(\"success\", true);\n response.put(\"service\", \"ConfigController\");\n response.put(\"status\", \"healthy\");\n response.put(\"timestamp\", System.currentTimeMillis());\n\n return ResponseEntity.ok(response);\n }\n\n /**\n * 初始化AI配置(如果不存在则创建默认值)\n * @return 初始化结果\n */\n @PostMapping(\"/init-ai\")\n public ResponseEntity<Map<String, Object>> initAiConfig(@RequestBody(required = false) Map<String, String> requestBody) {\n Map<String, Object> response = new HashMap<>();\n\n try {\n // 默认配置\n String baseUrl = requestBody != null && requestBody.containsKey(\"BASE_URL\") \n ? requestBody.get(\"BASE_URL\") : \"https://api.deepseek.com/v1\";\n String apiKey = requestBody != null && requestBody.containsKey(\"API_KEY\") \n ? requestBody.get(\"API_KEY\") : \"\";\n String model = requestBody != null && requestBody.containsKey(\"MODEL\") \n ? requestBody.get(\"MODEL\") : \"deepseek-chat\";\n\n // 创建或更新配置\n configService.upsertConfig(\"BASE_URL\", baseUrl, \"ai\", \"AI API基础URL\");\n configService.upsertConfig(\"API_KEY\", apiKey, \"ai\", \"AI API密钥\");\n configService.upsertConfig(\"MODEL\", model, \"ai\", \"AI模型名称\");\n\n response.put(\"success\", true);\n response.put(\"message\", \"AI配置初始化成功\");\n response.put(\"configs\", Map.of(\"BASE_URL\", baseUrl, \"API_KEY\", apiKey.isEmpty() ? \"(未设置)\" : \"(已设置)\", \"MODEL\", model));\n\n log.info(\"AI配置初始化完成: BASE_URL={}, MODEL={}\", baseUrl, model);\n return ResponseEntity.ok(response);\n\n } catch (Exception e) {\n log.error(\"初始化AI配置失败\", e);\n response.put(\"success\", false);\n response.put(\"message\", \"初始化AI配置失败: \" + e.getMessage());\n return ResponseEntity.internalServerError().body(response);\n }\n }\n}\n"}, "5dafc5a960c7ccf1992bbdda79f6ff9b9f0dc340": {"t": 1785755751.775804, "n": 6917, "h": "3871eced2e2d473e", "c": "package com.getjobs.application.controller;\n\nimport com.getjobs.application.service.ConfigService;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.http.ResponseEntity;\nimport org.springframework.web.bind.annotation.*;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\n/**\n * 配置控制器\n * 提供配置管理的REST API接口\n */\n@Slf4j\n@RestController\n@RequestMapping(\"/api/config\")\n@CrossOrigin(origins = \"*\")\npublic class ConfigController {\n\n @Autowired\n private ConfigService configService;\n\n /**\n * 获取所有配置\n * @return 配置Map\n */\n @GetMapping\n public ResponseEntity<Map<String, Object>> getAllConfigs() {\n Map<String, Object> response = new HashMap<>();\n\n try {\n Map<String, String> configs = configService.getAllConfigsAsMap();\n\n response.put(\"success\", true);\n response.put(\"data\", configs);\n response.put(\"message\", \"获取配置成功\");\n\n return ResponseEntity.ok(response);\n\n } catch (Exception e) {\n log.error(\"获取配置失败\", e);\n response.put(\"success\", false);\n response.put(\"message\", \"获取配置失败: \" + e.getMessage());\n return ResponseEntity.internalServerError().body(response);\n }\n }\n\n /**\n * 根据配置键获取单个配置\n * @param key 配置键\n * @return 配置值\n */\n @GetMapping(\"/{key}\")\n public ResponseEntity<Map<String, Object>> getConfigByKey(@PathVariable String key) {\n Map<String, Object> response = new HashMap<>();\n\n try {\n var config = configService.getConfigByKey(key);\n\n if (config != null) {\n response.put(\"success\", true);\n response.put(\"data\", config);\n response.put(\"message\", \"获取配置成功\");\n return ResponseEntity.ok(response);\n } else {\n response.put(\"success\", false);\n response.put(\"message\", \"配置不存在\");\n return ResponseEntity.notFound().build();\n }\n\n } catch (Exception e) {\n log.error(\"获取配置失败: {}\", key, e);\n response.put(\"success\", false);\n response.put(\"message\", \"获取配置失败: \" + e.getMessage());\n return ResponseEntity.internalServerError().body(response);\n }\n }\n\n /**\n * 批量更新配置\n * @param configMap 配置Map,key为config_key,value为config_value\n * @return 更新结果\n */\n @PostMapping\n public ResponseEntity<Map<String, Object>> batchUpdateConfigs(@RequestBody Map<String, String> configMap) {\n Map<String, Object> response = new HashMap<>();\n\n try {\n if (configMap == null || configMap.isEmpty()) {\n response.put(\"success\", false);\n response.put(\"message\", \"配置数据不能为空\");\n return ResponseEntity.badRequest().body(response);\n }\n\n int updateCount = configService.batchUpdateConfigs(configMap);\n\n response.put(\"success\", true);\n response.put(\"message\", \"配置更新成功\");\n response.put(\"updateCount\", updateCount);\n\n log.info(\"批量更新配置成功,共更新 {} 项\", updateCount);\n return ResponseEntity.ok(response);\n\n } catch (Exception e) {\n log.error(\"批量更新配置失败\", e);\n response.put(\"success\", false);\n response.put(\"message\", \"配置更新失败: \" + e.getMessage());\n return ResponseEntity.internalServerError().body(response);\n }\n }\n\n /**\n * 更新单个配置\n * @param key 配置键\n * @param requestBody 请求体包含value\n * @return 更新结果\n */\n @PutMapping(\"/{key}\")\n public ResponseEntity<Map<String, Object>> updateConfig(\n @PathVariable String key,\n @RequestBody Map<String, String> requestBody) {\n\n Map<String, Object> response = new HashMap<>();\n\n try {\n String value = requestBody.get(\"value\");\n\n if (value == null) {\n response.put(\"success\", false);\n response.put(\"message\", \"配置值不能为空\");\n return ResponseEntity.badRequest().body(response);\n }\n\n boolean success = configService.updateConfig(key, value);\n\n if (success) {\n response.put(\"success\", true);\n response.put(\"message\", \"配置更新成功\");\n return ResponseEntity.ok(response);\n } else {\n response.put(\"success\", false);\n response.put(\"message\", \"配置更新失败,配置键可能不存在\");\n return ResponseEntity.badRequest().body(response);\n }\n\n } catch (Exception e) {\n log.error(\"更新配置失败: {}\", key, e);\n response.put(\"success\", false);\n response.put(\"message\", \"配置更新失败: \" + e.getMessage());\n return ResponseEntity.internalServerError().body(response);\n }\n }\n\n /**\n * 健康检查接口\n * @return 服务状态\n */\n @GetMapping(\"/health\")\n public ResponseEntity<Map<String, Object>> healthCheck() {\n Map<String, Object> response = new HashMap<>();\n response.put(\"success\", true);\n response.put(\"service\", \"ConfigController\");\n response.put(\"status\", \"healthy\");\n response.put(\"timestamp\", System.currentTimeMillis());\n\n return ResponseEntity.ok(response);\n }\n\n /**\n * 初始化AI配置(如果不存在则创建默认值)\n * @return 初始化结果\n */\n @PostMapping(\"/init-ai\")\n public ResponseEntity<Map<String, Object>> initAiConfig(@RequestBody(required = false) Map<String, String> requestBody) {\n Map<String, Object> response = new HashMap<>();\n\n try {\n // 默认配置\n String baseUrl = requestBody != null && requestBody.containsKey(\"BASE_URL\") \n ? requestBody.get(\"BASE_URL\") : \"https://api.deepseek.com/v1\";\n String apiKey = requestBody != null && requestBody.containsKey(\"API_KEY\") \n ? requestBody.get(\"API_KEY\") : \"\";\n String model = requestBody != null && requestBody.containsKey(\"MODEL\") \n ? requestBody.get(\"MODEL\") : \"deepseek-chat\";\n\n // 创建或更新配置\n configService.upsertConfig(\"BASE_URL\", baseUrl, \"ai\", \"AI API基础URL\");\n configService.upsertConfig(\"API_KEY\", apiKey, \"ai\", \"AI API密钥\");\n configService.upsertConfig(\"MODEL\", model, \"ai\", \"AI模型名称\");\n\n response.put(\"success\", true);\n response.put(\"message\", \"AI配置初始化成功\");\n response.put(\"configs\", Map.of(\"BASE_URL\", baseUrl, \"API_KEY\", apiKey.isEmpty() ? \"(未设置)\" : \"(已设置)\", \"MODEL\", model));\n\n log.info(\"AI配置初始化完成: BASE_URL={}, MODEL={}\", baseUrl, model);\n return ResponseEntity.ok(response);\n\n } catch (Exception e) {\n log.error(\"初始化AI配置失败\", e);\n response.put(\"success\", false);\n response.put(\"message\", \"初始化AI配置失败: \" + e.getMessage());\n return ResponseEntity.internalServerError().body(response);\n }\n }\n}\n"}, "745ec46d2bac9e9c68d0caaf89b1db7845e266e3": {"t": 1785755753.381198, "n": 5277, "h": "8015603423d78196", "c": "package com.getjobs.application.controller;\n\nimport com.getjobs.application.service.ConfigService;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.http.ResponseEntity;\nimport org.springframework.web.bind.annotation.*;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\n/**\n * 配置控制器\n * 提供配置管理的REST API接口\n */\n@Slf4j\n@RestController\n@RequestMapping(\"/api/config\")\n@CrossOrigin(origins = \"*\")\npublic class ConfigController {\n\n @Autowired\n private ConfigService configService;\n\n /**\n * 获取所有配置\n * @return 配置Map\n */\n @GetMapping\n public ResponseEntity<Map<String, Object>> getAllConfigs() {\n Map<String, Object> response = new HashMap<>();\n\n try {\n Map<String, String> configs = configService.getAllConfigsAsMap();\n\n response.put(\"success\", true);\n response.put(\"data\", configs);\n response.put(\"message\", \"获取配置成功\");\n\n return ResponseEntity.ok(response);\n\n } catch (Exception e) {\n log.error(\"获取配置失败\", e);\n response.put(\"success\", false);\n response.put(\"message\", \"获取配置失败: \" + e.getMessage());\n return ResponseEntity.internalServerError().body(response);\n }\n }\n\n /**\n * 根据配置键获取单个配置\n * @param key 配置键\n * @return 配置值\n */\n @GetMapping(\"/{key}\")\n public ResponseEntity<Map<String, Object>> getConfigByKey(@PathVariable String key) {\n Map<String, Object> response = new HashMap<>();\n\n try {\n var config = configService.getConfigByKey(key);\n\n if (config != null) {\n response.put(\"success\", true);\n response.put(\"data\", config);\n response.put(\"message\", \"获取配置成功\");\n return ResponseEntity.ok(response);\n } else {\n response.put(\"success\", false);\n response.put(\"message\", \"配置不存在\");\n return ResponseEntity.notFound().build();\n }\n\n } catch (Exception e) {\n log.error(\"获取配置失败: {}\", key, e);\n response.put(\"success\", false);\n response.put(\"message\", \"获取配置失败: \" + e.getMessage());\n return ResponseEntity.internalServerError().body(response);\n }\n }\n\n /**\n * 批量更新配置\n * @param configMap 配置Map,key为config_key,value为config_value\n * @return 更新结果\n */\n @PostMapping\n public ResponseEntity<Map<String, Object>> batchUpdateConfigs(@RequestBody Map<String, String> configMap) {\n Map<String, Object> response = new HashMap<>();\n\n try {\n if (configMap == null || configMap.isEmpty()) {\n response.put(\"success\", false);\n response.put(\"message\", \"配置数据不能为空\");\n return ResponseEntity.badRequest().body(response);\n }\n\n int updateCount = configService.batchUpdateConfigs(configMap);\n\n response.put(\"success\", true);\n response.put(\"message\", \"配置更新成功\");\n response.put(\"updateCount\", updateCount);\n\n log.info(\"批量更新配置成功,共更新 {} 项\", updateCount);\n return ResponseEntity.ok(response);\n\n } catch (Exception e) {\n log.error(\"批量更新配置失败\", e);\n response.put(\"success\", false);\n response.put(\"message\", \"配置更新失败: \" + e.getMessage());\n return ResponseEntity.internalServerError().body(response);\n }\n }\n\n /**\n * 更新单个配置\n * @param key 配置键\n * @param requestBody 请求体包含value\n * @return 更新结果\n */\n @PutMapping(\"/{key}\")\n public ResponseEntity<Map<String, Object>> updateConfig(\n @PathVariable String key,\n @RequestBody Map<String, String> requestBody) {\n\n Map<String, Object> response = new HashMap<>();\n\n try {\n String value = requestBody.get(\"value\");\n\n if (value == null) {\n response.put(\"success\", false);\n response.put(\"message\", \"配置值不能为空\");\n return ResponseEntity.badRequest().body(response);\n }\n\n boolean success = configService.updateConfig(key, value);\n\n if (success) {\n response.put(\"success\", true);\n response.put(\"message\", \"配置更新成功\");\n return ResponseEntity.ok(response);\n } else {\n response.put(\"success\", false);\n response.put(\"message\", \"配置更新失败,配置键可能不存在\");\n return ResponseEntity.badRequest().body(response);\n }\n\n } catch (Exception e) {\n log.error(\"更新配置失败: {}\", key, e);\n response.put(\"success\", false);\n response.put(\"message\", \"配置更新失败: \" + e.getMessage());\n return ResponseEntity.internalServerError().body(response);\n }\n }\n\n /**\n * 健康检查接口\n * @return 服务状态\n */\n @GetMapping(\"/health\")\n public ResponseEntity<Map<String, Object>> healthCheck() {\n Map<String, Object> response = new HashMap<>();\n response.put(\"success\", true);\n response.put(\"service\", \"ConfigController\");\n response.put(\"status\", \"healthy\");\n response.put(\"timestamp\", System.currentTimeMillis());\n\n return ResponseEntity.ok(response);\n }\n}\n"}, "16fe1d3685abaa2fec2a3283798d572787ef500f": {"t": 1785755754.8174026, "n": 5381, "h": "cd04252a42b05be9", "c": "package com.getjobs.application.controller;\n\nimport com.getjobs.application.service.ConfigService;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.http.ResponseEntity;\nimport org.springframework.web.bind.annotation.*;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\n/**\n * 配置控制器\n * 提供配置管理的REST API接口\n */\n@RestController\n@RequestMapping(\"/api/config\")\n@CrossOrigin(origins = \"*\")\npublic class ConfigController {\n\n private static final Logger log = LoggerFactory.getLogger(ConfigController.class);\n\n @Autowired\n private ConfigService configService;\n\n /**\n * 获取所有配置\n * @return 配置Map\n */\n @GetMapping\n public ResponseEntity<Map<String, Object>> getAllConfigs() {\n Map<String, Object> response = new HashMap<>();\n\n try {\n Map<String, String> configs = configService.getAllConfigsAsMap();\n\n response.put(\"success\", true);\n response.put(\"data\", configs);\n response.put(\"message\", \"获取配置成功\");\n\n return ResponseEntity.ok(response);\n\n } catch (Exception e) {\n log.error(\"获取配置失败\", e);\n response.put(\"success\", false);\n response.put(\"message\", \"获取配置失败: \" + e.getMessage());\n return ResponseEntity.internalServerError().body(response);\n }\n }\n\n /**\n * 根据配置键获取单个配置\n * @param key 配置键\n * @return 配置值\n */\n @GetMapping(\"/{key}\")\n public ResponseEntity<Map<String, Object>> getConfigByKey(@PathVariable String key) {\n Map<String, Object> response = new HashMap<>();\n\n try {\n var config = configService.getConfigByKey(key);\n\n if (config != null) {\n response.put(\"success\", true);\n response.put(\"data\", config);\n response.put(\"message\", \"获取配置成功\");\n return ResponseEntity.ok(response);\n } else {\n response.put(\"success\", false);\n response.put(\"message\", \"配置不存在\");\n return ResponseEntity.notFound().build();\n }\n\n } catch (Exception e) {\n log.error(\"获取配置失败: {}\", key, e);\n response.put(\"success\", false);\n response.put(\"message\", \"获取配置失败: \" + e.getMessage());\n return ResponseEntity.internalServerError().body(response);\n }\n }\n\n /**\n * 批量更新配置\n * @param configMap 配置Map,key为config_key,value为config_value\n * @return 更新结果\n */\n @PostMapping\n public ResponseEntity<Map<String, Object>> batchUpdateConfigs(@RequestBody Map<String, String> configMap) {\n Map<String, Object> response = new HashMap<>();\n\n try {\n if (configMap == null || configMap.isEmpty()) {\n response.put(\"success\", false);\n response.put(\"message\", \"配置数据不能为空\");\n return ResponseEntity.badRequest().body(response);\n }\n\n int updateCount = configService.batchUpdateConfigs(configMap);\n\n response.put(\"success\", true);\n response.put(\"message\", \"配置更新成功\");\n response.put(\"updateCount\", updateCount);\n\n log.info(\"批量更新配置成功,共更新 {} 项\", updateCount);\n return ResponseEntity.ok(response);\n\n } catch (Exception e) {\n log.error(\"批量更新配置失败\", e);\n response.put(\"success\", false);\n response.put(\"message\", \"配置更新失败: \" + e.getMessage());\n return ResponseEntity.internalServerError().body(response);\n }\n }\n\n /**\n * 更新单个配置\n * @param key 配置键\n * @param requestBody 请求体包含value\n * @return 更新结果\n */\n @PutMapping(\"/{key}\")\n public ResponseEntity<Map<String, Object>> updateConfig(\n @PathVariable String key,\n @RequestBody Map<String, String> requestBody) {\n\n Map<String, Object> response = new HashMap<>();\n\n try {\n String value = requestBody.get(\"value\");\n\n if (value == null) {\n response.put(\"success\", false);\n response.put(\"message\", \"配置值不能为空\");\n return ResponseEntity.badRequest().body(response);\n }\n\n boolean success = configService.updateConfig(key, value);\n\n if (success) {\n response.put(\"success\", true);\n response.put(\"message\", \"配置更新成功\");\n return ResponseEntity.ok(response);\n } else {\n response.put(\"success\", false);\n response.put(\"message\", \"配置更新失败,配置键可能不存在\");\n return ResponseEntity.badRequest().body(response);\n }\n\n } catch (Exception e) {\n log.error(\"更新配置失败: {}\", key, e);\n response.put(\"success\", false);\n response.put(\"message\", \"配置更新失败: \" + e.getMessage());\n return ResponseEntity.internalServerError().body(response);\n }\n }\n\n /**\n * 健康检查接口\n * @return 服务状态\n */\n @GetMapping(\"/health\")\n public ResponseEntity<Map<String, Object>> healthCheck() {\n Map<String, Object> response = new HashMap<>();\n response.put(\"success\", true);\n response.put(\"service\", \"ConfigController\");\n response.put(\"status\", \"healthy\");\n response.put(\"timestamp\", System.currentTimeMillis());\n\n return ResponseEntity.ok(response);\n }\n}\n"}}}}