{"repo": "aetik-yue/chinasoft-project-group23", "files": {"backend/src/main/java/com/chinasoft/smokesensor/client/LlmClient.java": {"128cbf1d8783403458e26c8850562e3b58b8810f": {"t": 1785745685.3527782, "n": 4232, "h": "299e0af72cb474b2", "c": "package com.chinasoft.smokesensor.client;\n\nimport com.chinasoft.smokesensor.config.LlmProperties;\nimport com.chinasoft.smokesensor.config.ApiKeyEncryptor;\nimport com.chinasoft.smokesensor.entity.SystemSetting;\nimport com.chinasoft.smokesensor.repository.SystemSettingRepository;\nimport jakarta.annotation.PostConstruct;\nimport java.util.List;\nimport java.util.Map;\nimport lombok.RequiredArgsConstructor;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.http.MediaType;\nimport org.springframework.stereotype.Component;\nimport org.springframework.web.client.RestClient;\n\n/**\n * DeepSeek(OpenAI 兼容)大模型客户端,封装 chat completions + function calling 调用。\n *\n *

调用 {@code POST {base-url}/v1/chat/completions},请求体含 {@code model / messages / tools / tool_choice},\n * 响应 {@code choices[0].message}(含 {@code content} 或 {@code tool_calls})。\n *\n *

本类只负责 HTTP 调用与响应解析,工具执行与多轮编排由 {@code AgentToolService} 完成。\n *\n *

降级:{@code qq.llm.enabled=false} 或未配置 api-key 时 {@link #isEnabled} 返回 false,\n * 调用方走规则回退。调用失败只 warn 不抛异常,返回 null。\n */\n@Slf4j\n@Component\n@RequiredArgsConstructor\npublic class LlmClient {\n\n private final LlmProperties properties;\n private final SystemSettingRepository systemSettingRepository;\n private final ApiKeyEncryptor apiKeyEncryptor;\n\n private RestClient restClient;\n\n @PostConstruct\n void init() {\n if (properties.getBaseUrl() != null && !properties.getBaseUrl().isBlank()) {\n this.restClient = RestClient.builder()\n .baseUrl(properties.getBaseUrl())\n .build();\n }\n }\n\n /**\n * 是否启用(已 enabled 且 api-key 非空且 restClient 已初始化)。\n */\n public boolean isEnabled() {\n String apiKey = getApiKey();\n return properties.isEnabled()\n && restClient != null\n && apiKey != null\n && !apiKey.isBlank();\n }\n\n private String getApiKey() {\n String dbKey = systemSettingRepository.findBySettingKey(\"deepseek_api_key\")\n .map(SystemSetting::getSettingValue)\n .orElse(null);\n if (dbKey != null && !dbKey.isBlank()) {\n return apiKeyEncryptor.decrypt(dbKey);\n }\n return properties.getApiKey();\n }\n\n /**\n * 调用 chat completions,返回 assistant 消息(含 content 或 tool_calls)。\n *\n * @param messages 对话消息列表(含历史 + 上一轮 assistant tool_calls + tool 结果)\n * @param tools 工具 schema 列表(OpenAI function 格式),为空则不传 tools\n * @return assistant 消息 Map,或 null(调用失败 / 无响应)\n */\n @SuppressWarnings(\"unchecked\")\n public Map chat(List> messages, List> tools) {\n if (!isEnabled()) {\n return null;\n }\n try {\n Map body = new java.util.LinkedHashMap<>();\n body.put(\"model\", properties.getModel());\n body.put(\"messages\", messages);\n if (tools != null && !tools.isEmpty()) {\n body.put(\"tools\", tools);\n body.put(\"tool_choice\", \"auto\");\n }\n\n Map response = restClient.post()\n .uri(\"/v1/chat/completions\")\n .header(\"Authorization\", \"Bearer \" + getApiKey())\n .contentType(MediaType.APPLICATION_JSON)\n .body(body)\n .retrieve()\n .body(Map.class);\n return extractMessage(response);\n } catch (Exception e) {\n log.warn(\"DeepSeek 调用失败: reason={}\", e.getMessage());\n return null;\n }\n }\n\n /**\n * 从响应中提取 choices[0].message。\n */\n @SuppressWarnings(\"unchecked\")\n private Map extractMessage(Map response) {\n if (response == null) {\n return null;\n }\n Object choices = response.get(\"choices\");\n if (choices instanceof List list && !list.isEmpty() && list.get(0) instanceof Map first) {\n Object message = first.get(\"message\");\n if (message instanceof Map msg) {\n return (Map) msg;\n }\n }\n log.warn(\"DeepSeek 响应格式异常: {}\", response);\n return null;\n }\n}\n"}, "a101dd944b079def1d8450a4b94b00df32d52e1e": {"t": 1785755705.9064398, "n": 4232, "h": "299e0af72cb474b2", "c": "package com.chinasoft.smokesensor.client;\n\nimport com.chinasoft.smokesensor.config.LlmProperties;\nimport com.chinasoft.smokesensor.config.ApiKeyEncryptor;\nimport com.chinasoft.smokesensor.entity.SystemSetting;\nimport com.chinasoft.smokesensor.repository.SystemSettingRepository;\nimport jakarta.annotation.PostConstruct;\nimport java.util.List;\nimport java.util.Map;\nimport lombok.RequiredArgsConstructor;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.http.MediaType;\nimport org.springframework.stereotype.Component;\nimport org.springframework.web.client.RestClient;\n\n/**\n * DeepSeek(OpenAI 兼容)大模型客户端,封装 chat completions + function calling 调用。\n *\n *

调用 {@code POST {base-url}/v1/chat/completions},请求体含 {@code model / messages / tools / tool_choice},\n * 响应 {@code choices[0].message}(含 {@code content} 或 {@code tool_calls})。\n *\n *

本类只负责 HTTP 调用与响应解析,工具执行与多轮编排由 {@code AgentToolService} 完成。\n *\n *

降级:{@code qq.llm.enabled=false} 或未配置 api-key 时 {@link #isEnabled} 返回 false,\n * 调用方走规则回退。调用失败只 warn 不抛异常,返回 null。\n */\n@Slf4j\n@Component\n@RequiredArgsConstructor\npublic class LlmClient {\n\n private final LlmProperties properties;\n private final SystemSettingRepository systemSettingRepository;\n private final ApiKeyEncryptor apiKeyEncryptor;\n\n private RestClient restClient;\n\n @PostConstruct\n void init() {\n if (properties.getBaseUrl() != null && !properties.getBaseUrl().isBlank()) {\n this.restClient = RestClient.builder()\n .baseUrl(properties.getBaseUrl())\n .build();\n }\n }\n\n /**\n * 是否启用(已 enabled 且 api-key 非空且 restClient 已初始化)。\n */\n public boolean isEnabled() {\n String apiKey = getApiKey();\n return properties.isEnabled()\n && restClient != null\n && apiKey != null\n && !apiKey.isBlank();\n }\n\n private String getApiKey() {\n String dbKey = systemSettingRepository.findBySettingKey(\"deepseek_api_key\")\n .map(SystemSetting::getSettingValue)\n .orElse(null);\n if (dbKey != null && !dbKey.isBlank()) {\n return apiKeyEncryptor.decrypt(dbKey);\n }\n return properties.getApiKey();\n }\n\n /**\n * 调用 chat completions,返回 assistant 消息(含 content 或 tool_calls)。\n *\n * @param messages 对话消息列表(含历史 + 上一轮 assistant tool_calls + tool 结果)\n * @param tools 工具 schema 列表(OpenAI function 格式),为空则不传 tools\n * @return assistant 消息 Map,或 null(调用失败 / 无响应)\n */\n @SuppressWarnings(\"unchecked\")\n public Map chat(List> messages, List> tools) {\n if (!isEnabled()) {\n return null;\n }\n try {\n Map body = new java.util.LinkedHashMap<>();\n body.put(\"model\", properties.getModel());\n body.put(\"messages\", messages);\n if (tools != null && !tools.isEmpty()) {\n body.put(\"tools\", tools);\n body.put(\"tool_choice\", \"auto\");\n }\n\n Map response = restClient.post()\n .uri(\"/v1/chat/completions\")\n .header(\"Authorization\", \"Bearer \" + getApiKey())\n .contentType(MediaType.APPLICATION_JSON)\n .body(body)\n .retrieve()\n .body(Map.class);\n return extractMessage(response);\n } catch (Exception e) {\n log.warn(\"DeepSeek 调用失败: reason={}\", e.getMessage());\n return null;\n }\n }\n\n /**\n * 从响应中提取 choices[0].message。\n */\n @SuppressWarnings(\"unchecked\")\n private Map extractMessage(Map response) {\n if (response == null) {\n return null;\n }\n Object choices = response.get(\"choices\");\n if (choices instanceof List list && !list.isEmpty() && list.get(0) instanceof Map first) {\n Object message = first.get(\"message\");\n if (message instanceof Map msg) {\n return (Map) msg;\n }\n }\n log.warn(\"DeepSeek 响应格式异常: {}\", response);\n return null;\n }\n}\n"}, "ff85f054131b21bbc73583bdb7acd20d5a4cdcf4": {"t": 1785755707.230306, "n": 3582, "h": "bbf28979088082ea", "c": "package com.chinasoft.smokesensor.client;\n\nimport com.chinasoft.smokesensor.config.LlmProperties;\nimport jakarta.annotation.PostConstruct;\nimport java.util.List;\nimport java.util.Map;\nimport lombok.RequiredArgsConstructor;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.http.MediaType;\nimport org.springframework.stereotype.Component;\nimport org.springframework.web.client.RestClient;\n\n/**\n * DeepSeek(OpenAI 兼容)大模型客户端,封装 chat completions + function calling 调用。\n *\n *

调用 {@code POST {base-url}/v1/chat/completions},请求体含 {@code model / messages / tools / tool_choice},\n * 响应 {@code choices[0].message}(含 {@code content} 或 {@code tool_calls})。\n *\n *

本类只负责 HTTP 调用与响应解析,工具执行与多轮编排由 {@code AgentToolService} 完成。\n *\n *

降级:{@code qq.llm.enabled=false} 或未配置 api-key 时 {@link #isEnabled} 返回 false,\n * 调用方走规则回退。调用失败只 warn 不抛异常,返回 null。\n */\n@Slf4j\n@Component\n@RequiredArgsConstructor\npublic class LlmClient {\n\n private final LlmProperties properties;\n\n private RestClient restClient;\n\n @PostConstruct\n void init() {\n if (properties.getBaseUrl() != null && !properties.getBaseUrl().isBlank()) {\n this.restClient = RestClient.builder()\n .baseUrl(properties.getBaseUrl())\n .build();\n }\n }\n\n /**\n * 是否启用(已 enabled 且 api-key 非空且 restClient 已初始化)。\n */\n public boolean isEnabled() {\n return properties.isEnabled()\n && restClient != null\n && properties.getApiKey() != null\n && !properties.getApiKey().isBlank();\n }\n\n /**\n * 调用 chat completions,返回 assistant 消息(含 content 或 tool_calls)。\n *\n * @param messages 对话消息列表(含历史 + 上一轮 assistant tool_calls + tool 结果)\n * @param tools 工具 schema 列表(OpenAI function 格式),为空则不传 tools\n * @return assistant 消息 Map,或 null(调用失败 / 无响应)\n */\n @SuppressWarnings(\"unchecked\")\n public Map chat(List> messages, List> tools) {\n if (!isEnabled()) {\n return null;\n }\n try {\n Map body = new java.util.LinkedHashMap<>();\n body.put(\"model\", properties.getModel());\n body.put(\"messages\", messages);\n if (tools != null && !tools.isEmpty()) {\n body.put(\"tools\", tools);\n body.put(\"tool_choice\", \"auto\");\n }\n\n Map response = restClient.post()\n .uri(\"/v1/chat/completions\")\n .header(\"Authorization\", \"Bearer \" + properties.getApiKey())\n .contentType(MediaType.APPLICATION_JSON)\n .body(body)\n .retrieve()\n .body(Map.class);\n return extractMessage(response);\n } catch (Exception e) {\n log.warn(\"DeepSeek 调用失败: reason={}\", e.getMessage());\n return null;\n }\n }\n\n /**\n * 从响应中提取 choices[0].message。\n */\n @SuppressWarnings(\"unchecked\")\n private Map extractMessage(Map response) {\n if (response == null) {\n return null;\n }\n Object choices = response.get(\"choices\");\n if (choices instanceof List list && !list.isEmpty() && list.get(0) instanceof Map first) {\n Object message = first.get(\"message\");\n if (message instanceof Map msg) {\n return (Map) msg;\n }\n }\n log.warn(\"DeepSeek 响应格式异常: {}\", response);\n return null;\n }\n}\n"}}}}