From 21c5b8f761450f8991ed5c45b8f5cbf351f09e65 Mon Sep 17 00:00:00 2001 From: cat_shark <1716967236@qq.com> Date: Fri, 3 Jul 2026 23:52:32 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E6=8A=A5=E5=91=8A)=EF=BC=9AAI=20=E8=81=9A?= =?UTF-8?q?=E5=90=88=E6=AE=8B=E7=89=87=E7=94=9F=E6=88=90=E6=8A=A5=E5=91=8A?= =?UTF-8?q?=E8=8D=89=E7=A8=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AiServiceClient:调用 lpt-ai 独立服务,未配置/失败时返回 empty - GET /study-sessions/{sessionNum}/report-draft:AI 聚合(含预期对比),降级为残片拼接 - 草稿仅作为编辑起点,最终报告仍由用户确认后保存 - application.yml 增加 lpt.ai-service.url 配置 --- .../controller/StudySessionController.java | 8 ++ .../service/impl/AiServiceClient.java | 92 +++++++++++++++++++ .../impl/StudySessionsServiceImpl.java | 29 ++++++ src/main/resources/application.yml | 6 +- 4 files changed, 134 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/guo/learningprogresstracker/service/impl/AiServiceClient.java diff --git a/src/main/java/com/guo/learningprogresstracker/controller/StudySessionController.java b/src/main/java/com/guo/learningprogresstracker/controller/StudySessionController.java index 0de2e1c..7c26c59 100644 --- a/src/main/java/com/guo/learningprogresstracker/controller/StudySessionController.java +++ b/src/main/java/com/guo/learningprogresstracker/controller/StudySessionController.java @@ -99,4 +99,12 @@ public class StudySessionController { return CommonResult.success(allFragments); } + + /** + * 生成学习报告草稿:AI 聚合残片(不可用时降级为拼接),返回给前端作为编辑起点 + */ + @GetMapping("/{sessionNum}/report-draft") + public CommonResult getReportDraft(@PathVariable("sessionNum") String sessionNum) throws ErrorParameterException { + return CommonResult.success("请求成功", studySessionsServiceImpl.generateReportDraft(sessionNum)); + } } diff --git a/src/main/java/com/guo/learningprogresstracker/service/impl/AiServiceClient.java b/src/main/java/com/guo/learningprogresstracker/service/impl/AiServiceClient.java new file mode 100644 index 0000000..f0ddbf0 --- /dev/null +++ b/src/main/java/com/guo/learningprogresstracker/service/impl/AiServiceClient.java @@ -0,0 +1,92 @@ +package com.guo.learningprogresstracker.service.impl; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.annotation.PostConstruct; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.Duration; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +/** + * lpt-ai 独立 AI 服务客户端。 + *

通过 {@code lpt.ai-service.url} 配置服务地址;未配置或调用失败时返回 empty,由调用方降级。

+ */ +@Slf4j +@Component +@ConfigurationProperties(prefix = "lpt.ai-service") +@Setter +public class AiServiceClient { + + /** lpt-ai 服务地址,如 http://localhost:5199 */ + private String url; + /** 超时秒数 */ + private int timeoutSeconds = 90; + + private HttpClient httpClient; + private final ObjectMapper objectMapper = new ObjectMapper(); + + @PostConstruct + void init() { + httpClient = HttpClient.newBuilder() + .connectTimeout(Duration.ofSeconds(10)) + .build(); + if (isConfigured()) { + log.info("AiServiceClient 已配置: {}", url); + } else { + log.info("AiServiceClient 未配置,AI 聚合功能不可用(将降级为拼接)"); + } + } + + public boolean isConfigured() { + return url != null && !url.isBlank(); + } + + /** + * 调用 lpt-ai 将残片聚合为学习报告草稿。 + * + * @return 聚合后的报告文本;服务未配置或调用失败时返回 empty + */ + public Optional aggregateReport(String taskName, List fragments, String expectation) { + if (!isConfigured() || fragments == null || fragments.isEmpty()) { + return Optional.empty(); + } + try { + List> fragmentBodies = fragments.stream() + .map(content -> Map.of("content", content)) + .toList(); + Map body = expectation == null || expectation.isBlank() + ? Map.of("taskName", taskName, "fragments", fragmentBodies) + : Map.of("taskName", taskName, "fragments", fragmentBodies, "expectation", expectation); + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(url + "/ai/aggregate-report")) + .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 report = json.path("report").asText(null); + return Optional.ofNullable(report).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/StudySessionsServiceImpl.java b/src/main/java/com/guo/learningprogresstracker/service/impl/StudySessionsServiceImpl.java index d8d55a1..7ae0284 100644 --- a/src/main/java/com/guo/learningprogresstracker/service/impl/StudySessionsServiceImpl.java +++ b/src/main/java/com/guo/learningprogresstracker/service/impl/StudySessionsServiceImpl.java @@ -16,6 +16,7 @@ import com.guo.learningprogresstracker.mapStruct.StudySessionConvert; import com.guo.learningprogresstracker.mapper.StudyReportFragmentsMapper; import com.guo.learningprogresstracker.mapper.StudyReportsMapper; import com.guo.learningprogresstracker.mapper.StudySessionsMapper; +import com.guo.learningprogresstracker.service.StudyExpectationsService; import com.guo.learningprogresstracker.service.StudySessionsService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -41,6 +42,8 @@ public class StudySessionsServiceImpl extends ServiceImpl new ErrorParameterException("会话[" + sessionNum + "]不存在")); + + ArrayList fragments = getAllFragments(sessionNum); + if (fragments.isEmpty()) { + return ""; + } + + String taskName = tasksServiceImpl.getOneOpt( + Wrappers.lambdaQuery(TaskEntity.class).eq(TaskEntity::getTaskNum, session.getTaskNum())) + .map(TaskEntity::getTaskName) + .orElse("学习任务"); + String expectation = Optional.ofNullable(studyExpectationsService.getBySessionNum(sessionNum)) + .map(e -> e.getDescription()) + .orElse(null); + + return aiServiceClient.aggregateReport(taskName, fragments, expectation) + .orElseGet(() -> String.join("\n", fragments)); + } + } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 62e203d..7226704 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -58,4 +58,8 @@ lpt: endpoint: api-key: model: claude-fable-5 - timeout-seconds: 60 \ No newline at end of file + timeout-seconds: 60 + # lpt-ai 独立服务地址(可选,未配置时报告草稿降级为残片拼接) + ai-service: + url: + timeout-seconds: 90 \ No newline at end of file