From cd5ee0b40459aef109d349c41ac02ec1a38c93d5 Mon Sep 17 00:00:00 2001 From: cat_shark <1716967236@qq.com> Date: Sat, 4 Jul 2026 11:14:27 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E6=80=9D=E7=BB=B4=E5=AF=BC=E5=9B=BE)?= =?UTF-8?q?=EF=BC=9AAI=20=E7=94=9F=E6=88=90=E6=8E=A5=E5=85=A5=20lpt-ai=20?= =?UTF-8?q?=E6=9C=8D=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RemoteAiMindMapClient 原为 TODO 空桩,依赖 lpt.ai.endpoint/api-key 配置,从未生效。现改为依赖 AiServiceClient(lpt.ai-service.url), 调用 lpt-ai 的 /ai/generate-mind-map 接口获取 AI 大纲。 变更: - AiServiceClient 新增 generateMindMap():调用 POST /ai/generate-mind-map - RemoteAiMindMapClient 重写:注入 AiServiceClient,isAvailable() 改为检查 AiServiceClient.isConfigured(),generate() 用 lpt-ai 返回的大纲文本解析为 MindMapNode 树 - StandardMindMapServiceImpl.doGenerate():优先选 AI 客户端 (非 BUILTIN),AI 失败时自动降级到内置规则引擎 - application.yml:移除不再使用的 lpt.ai.endpoint/api-key 配置 --- .../service/impl/AiServiceClient.java | 40 ++++++++++ .../service/impl/RemoteAiMindMapClient.java | 78 ++++++++++--------- .../impl/StandardMindMapServiceImpl.java | 14 +++- src/main/resources/application.yml | 10 +-- 4 files changed, 97 insertions(+), 45 deletions(-) diff --git a/src/main/java/com/guo/learningprogresstracker/service/impl/AiServiceClient.java b/src/main/java/com/guo/learningprogresstracker/service/impl/AiServiceClient.java index f0ddbf0..7955c70 100644 --- a/src/main/java/com/guo/learningprogresstracker/service/impl/AiServiceClient.java +++ b/src/main/java/com/guo/learningprogresstracker/service/impl/AiServiceClient.java @@ -89,4 +89,44 @@ public class AiServiceClient { return Optional.empty(); } } + + /** + * 调用 lpt-ai 从学习数据生成思维导图大纲(缩进文本格式)。 + * + * @return 缩进大纲文本;服务未配置或调用失败时返回 empty + */ + public Optional generateMindMap(String taskName, String taskDescription, + List reports, List fragments) { + if (!isConfigured()) { + return Optional.empty(); + } + try { + Map body = Map.of( + "taskName", taskName, + "taskDescription", taskDescription != null ? taskDescription : "", + "reports", reports != null ? reports : List.of(), + "fragments", fragments != null ? fragments : List.of() + ); + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(url + "/ai/generate-mind-map")) + .timeout(Duration.ofSeconds(timeoutSeconds)) + .header("Content-Type", "application/json") + .POST(HttpRequest.BodyPublishers.ofString(objectMapper.writeValueAsString(body))) + .build(); + + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + if (response.statusCode() != 200) { + log.warn("lpt-ai 生成思维导图失败: status={}, body={}", response.statusCode(), + response.body() != null ? response.body().substring(0, Math.min(200, response.body().length())) : ""); + return Optional.empty(); + } + JsonNode json = objectMapper.readTree(response.body()); + String outline = json.path("outline").asText(null); + return Optional.ofNullable(outline).filter(s -> !s.isBlank()); + } catch (Exception e) { + log.warn("lpt-ai 思维导图调用异常,将降级: {}", e.getMessage()); + return Optional.empty(); + } + } } diff --git a/src/main/java/com/guo/learningprogresstracker/service/impl/RemoteAiMindMapClient.java b/src/main/java/com/guo/learningprogresstracker/service/impl/RemoteAiMindMapClient.java index 7b22be8..9de5dc9 100644 --- a/src/main/java/com/guo/learningprogresstracker/service/impl/RemoteAiMindMapClient.java +++ b/src/main/java/com/guo/learningprogresstracker/service/impl/RemoteAiMindMapClient.java @@ -6,55 +6,35 @@ import com.guo.learningprogresstracker.entity.TaskApplicationEntity; import com.guo.learningprogresstracker.entity.TaskEntity; import com.guo.learningprogresstracker.service.MindMapAiClient; import com.guo.learningprogresstracker.utils.MindMapNode; -import jakarta.annotation.PostConstruct; -import lombok.Setter; +import com.guo.learningprogresstracker.utils.MindMapTreeTool; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; import java.util.List; import java.util.Optional; +import java.util.stream.Collectors; /** - * 远程 AI 思维导图生成客户端(预留实现)。 - *

通过 {@code lpt.ai.endpoint} / {@code lpt.ai.api-key} 配置;未配置时不可用, - * 上层服务会回退到 {@link BuiltinMindMapGenerator}。

+ * 远程 AI 思维导图生成客户端。 + *

通过 lpt-ai 服务({@code lpt.ai-service.url})调用大语言模型生成思维导图。 + * 未配置 lpt-ai 时不可用,会回退到 {@link BuiltinMindMapGenerator}。

*/ @Slf4j @Component -@ConfigurationProperties(prefix = "lpt.ai") -@Setter +@RequiredArgsConstructor public class RemoteAiMindMapClient implements MindMapAiClient { - /** AI 服务端点 */ - private String endpoint; - /** API Key */ - private String apiKey; - /** 模型名称 */ - private String model = "claude-fable-5"; - /** 超时秒数 */ - private int timeoutSeconds = 60; - - private boolean enabled = false; - - @PostConstruct - void init() { - enabled = apiKey != null && !apiKey.isBlank() && endpoint != null && !endpoint.isBlank(); - if (enabled) { - log.info("RemoteAiMindMapClient 已启用: endpoint={}, model={}", endpoint, model); - } else { - log.info("RemoteAiMindMapClient 未配置,将使用内置生成器"); - } - } + private final AiServiceClient aiServiceClient; @Override public boolean isAvailable() { - return enabled; + return aiServiceClient.isConfigured(); } @Override public String generatorName() { - return "AI-" + model; + return "AI"; } @Override @@ -63,12 +43,40 @@ public class RemoteAiMindMapClient implements MindMapAiClient { List fragments, List applications, String clientHint) { - if (!enabled) { + if (!isAvailable()) { return Optional.empty(); } - // TODO: 调用远程 AI API,构造 prompt 发送 reports/fragments - // RestClient.create().post().uri(endpoint).body(prompt).retrieve() - log.warn("RemoteAiMindMapClient: 远程 AI 调用尚未实现,请配置 API Key 并实现请求逻辑"); - return Optional.empty(); + + // 提取报告和残片的内容文本 + List reportTexts = reports.stream() + .map(StudyReportsEntity::getContent) + .filter(c -> c != null && !c.isBlank()) + .collect(Collectors.toList()); + List fragmentTexts = fragments.stream() + .map(StudyReportFragmentsEntity::getContent) + .filter(c -> c != null && !c.isBlank()) + .collect(Collectors.toList()); + + if (reportTexts.isEmpty() && fragmentTexts.isEmpty()) { + return Optional.empty(); + } + + Optional optOutline = aiServiceClient.generateMindMap( + task.getTaskName(), + task.getTaskDescription(), + reportTexts, + fragmentTexts + ); + + if (optOutline.isEmpty() || optOutline.get().isBlank()) { + log.warn("RemoteAiMindMapClient: lpt-ai 返回空大纲,降级"); + return Optional.empty(); + } + + String outline = optOutline.get(); + MindMapNode root = MindMapTreeTool.parseOutline(outline); + log.info("RemoteAiMindMapClient: 为任务[{}]生成 AI 导图,共 {} 个节点,{} 层", + task.getTaskNum(), MindMapTreeTool.countNodes(root), MindMapTreeTool.maxDepth(root)); + return Optional.of(root); } } diff --git a/src/main/java/com/guo/learningprogresstracker/service/impl/StandardMindMapServiceImpl.java b/src/main/java/com/guo/learningprogresstracker/service/impl/StandardMindMapServiceImpl.java index 02ec4a8..434fe02 100644 --- a/src/main/java/com/guo/learningprogresstracker/service/impl/StandardMindMapServiceImpl.java +++ b/src/main/java/com/guo/learningprogresstracker/service/impl/StandardMindMapServiceImpl.java @@ -251,10 +251,10 @@ public class StandardMindMapServiceImpl implements StandardMindMapService { throw new OperationFailedException("任务[" + taskNum + "]没有学习报告或残片,无法生成思维导图"); } - // 找可用的 AI 客户端 + // 优先选 AI 客户端(非 BUILTIN),其次内置生成器 MindMapAiClient client = aiClients.stream() .filter(MindMapAiClient::isAvailable) - .findFirst() + .min(Comparator.comparing(c -> "BUILTIN".equals(c.generatorName()) ? 1 : 0)) .orElse(null); if (client == null) { @@ -262,6 +262,16 @@ public class StandardMindMapServiceImpl implements StandardMindMapService { } Optional optRoot = client.generate(task, reports, fragments, applications, null); + // AI 生成失败时尝试降级到内置生成器 + if (optRoot.isEmpty() && !"BUILTIN".equals(client.generatorName())) { + log.info("AI 思维导图生成失败,降级到内置生成器"); + MindMapAiClient fallback = aiClients.stream() + .filter(c -> "BUILTIN".equals(c.generatorName()) && c.isAvailable()) + .findFirst().orElse(null); + if (fallback != null) { + optRoot = fallback.generate(task, reports, fragments, applications, null); + } + } if (optRoot.isEmpty()) { throw new OperationFailedException("思维导图生成失败"); } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 7226704..cca62bf 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -52,14 +52,8 @@ management: exposure: include: health,info # 或 "*" -# 思维导图 AI 配置(可选,未配置时使用内置生成器) +# lpt-ai 独立 AI 服务(可选,未配置时降级为内置规则引擎) lpt: - ai: - endpoint: - api-key: - model: claude-fable-5 - timeout-seconds: 60 - # lpt-ai 独立服务地址(可选,未配置时报告草稿降级为残片拼接) ai-service: - url: + url: http://localhost:5199 timeout-seconds: 90 \ No newline at end of file