1 line
127 KiB
JSON
1 line
127 KiB
JSON
{"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"}}}} |