From 647ef436556cc21f7017f2f717fa58cc3e569ced Mon Sep 17 00:00:00 2001 From: luke Date: Sun, 22 Jun 2025 08:32:51 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E6=88=90llm=E6=8E=A5=E5=8F=A3?= =?UTF-8?q?=E8=BF=81=E7=A7=BB=E8=87=B3=E5=90=8E=E7=AB=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Readme.md | 3 +- pom.xml | 19 ++++ .../base/infrastructure/config/LLmConfig.java | 21 ++++ .../north/controller/LLMController.java | 93 +++++++++++++++ .../base/infrastructure/util/HttpHelper.java | 106 ++++++++++++++++++ .../application-dev-windows.properties | 2 + .../resources/application-docker.properties | 3 + src/main/resources/application.properties | 3 + src/test/resources/application.properties | 3 + 9 files changed, 252 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/knowledge/base/infrastructure/config/LLmConfig.java create mode 100644 src/main/java/com/knowledge/base/infrastructure/north/controller/LLMController.java create mode 100644 src/main/java/com/knowledge/base/infrastructure/util/HttpHelper.java diff --git a/Readme.md b/Readme.md index 82ed758..880b5e0 100644 --- a/Readme.md +++ b/Readme.md @@ -10,7 +10,8 @@ | 1.0.4 | 2025-06-17 | Luke.Ye | 上传文档记录入库 | | 1.0.5 | 2025-06-19 | Luke.Ye | 文档导入ES逻辑优化 | | 1.0.6 | 2025-06-20 | Luke.Ye | 完成对象存储删除接口,优化部分代码 | -| 1.1.0 | 2025-06-22 | Luke.Ye | 导入文档类型新增支持ppt & txt | +| 1.0.7 | 2025-06-22 | Luke.Ye | 导入文档类型新增支持ppt & txt | +| 1.1.0 | 2025-06-22 | Luke.Ye | 完成llm接口迁移至后端 | diff --git a/pom.xml b/pom.xml index 9f0a20e..d449ddd 100644 --- a/pom.xml +++ b/pom.xml @@ -47,6 +47,7 @@ 1.70 8.5.3 + 4.12.0 @@ -225,6 +226,19 @@ io.minio minio ${minio.version} + + + com.squareup.okhttp3 + okhttp + + + + + + + com.squareup.okhttp3 + okhttp + ${okhttp.version} @@ -381,6 +395,11 @@ minio + + com.squareup.okhttp3 + okhttp + + diff --git a/src/main/java/com/knowledge/base/infrastructure/config/LLmConfig.java b/src/main/java/com/knowledge/base/infrastructure/config/LLmConfig.java new file mode 100644 index 0000000..7e64378 --- /dev/null +++ b/src/main/java/com/knowledge/base/infrastructure/config/LLmConfig.java @@ -0,0 +1,21 @@ +package com.knowledge.base.infrastructure.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + + +/** + * @author Luke.ye + * @date 2025/6/22 08:15 + */ +@Data +@Component +@ConfigurationProperties(prefix = "llm.remote") +public class LLmConfig { + /** + * 远程LLM服务基础地址,如:https://llm.wisdompulse.cn + */ + private String baseUrl; +} + diff --git a/src/main/java/com/knowledge/base/infrastructure/north/controller/LLMController.java b/src/main/java/com/knowledge/base/infrastructure/north/controller/LLMController.java new file mode 100644 index 0000000..e03d90f --- /dev/null +++ b/src/main/java/com/knowledge/base/infrastructure/north/controller/LLMController.java @@ -0,0 +1,93 @@ +package com.knowledge.base.infrastructure.north.controller; + +import com.knowledge.base.infrastructure.util.HttpHelper; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.knowledge.base.infrastructure.config.LLmConfig; +import lombok.extern.slf4j.Slf4j; +import okhttp3.Response; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitter; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.Map; + + +/** + * @author Luke.ye + * @date 2025/6/22 08:08 + */ + +@Slf4j +@RestController +@RequestMapping("/api/v1/llm") +@RequiredArgsConstructor +public class LLMController { + + private final HttpHelper okHttpHelper; + private final LLmConfig llmConfig; + private final ObjectMapper objectMapper; + + /** + * POST /token + * 请求:{ password: xxx } + * 返回:{ token: xxx } + */ + @PostMapping("/token") + public Map getToken(@RequestBody Map body) throws Exception { + String url = llmConfig.getBaseUrl() + "/api/token"; + return okHttpHelper.postJson(url, body, null); + } + + /** + * GET /workspaces + * Header: Authorization: Bearer xxx + * 返回:{ workspaces: [...] } + */ + @GetMapping("/workspaces") + public Map getWorkspaces(@RequestHeader("Authorization") String token) throws Exception { + String url = llmConfig.getBaseUrl() + "/api/workspaces"; + return okHttpHelper.get(url, token); + } + + /** + * POST /workspace/{slug}/stream-chat + * Header: Authorization + * Body: { message: "...", attachments: [...] } + * 返回:text/event-stream + */ + @PostMapping(value = "/workspace/{slug}/stream-chat", produces = MediaType.TEXT_EVENT_STREAM_VALUE) + public ResponseBodyEmitter chatStream(@PathVariable String slug, + @RequestHeader("Authorization") String token, + @RequestBody Map body) { + String url = llmConfig.getBaseUrl() + "/api/workspace/" + slug + "/stream-chat"; + ResponseBodyEmitter emitter = new ResponseBodyEmitter(); + + new Thread(() -> { + try (Response resp = okHttpHelper.postStream(url, body, token); + BufferedReader reader = new BufferedReader(new InputStreamReader(resp.body().byteStream(), StandardCharsets.UTF_8))) { + + String line; + while ((line = reader.readLine()) != null) { + emitter.send(line + "\n"); + } + emitter.complete(); + } catch (Exception e) { + log.error("LLM 流式回答异常", e); + try { + emitter.send("data: {\"textResponse\":\"LLM服务异常\"}\n\n"); + } catch (Exception ignore) {} + emitter.completeWithError(e); + } + }).start(); + + return emitter; + } +} + diff --git a/src/main/java/com/knowledge/base/infrastructure/util/HttpHelper.java b/src/main/java/com/knowledge/base/infrastructure/util/HttpHelper.java new file mode 100644 index 0000000..76be105 --- /dev/null +++ b/src/main/java/com/knowledge/base/infrastructure/util/HttpHelper.java @@ -0,0 +1,106 @@ +package com.knowledge.base.infrastructure.util; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.extern.slf4j.Slf4j; +import okhttp3.*; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +/** + * OkHttp封装的 HTTP 客户端工具类(支持 JSON POST、GET、Token注入、泛型反序列化、流式请求) + */ +@Slf4j +@Component +public class HttpHelper { + + private final OkHttpClient client; + private final ObjectMapper objectMapper; + + public HttpHelper() { + this.client = new OkHttpClient.Builder() + .connectTimeout(20, TimeUnit.SECONDS) + .readTimeout(30, TimeUnit.SECONDS) + .build(); + this.objectMapper = new ObjectMapper(); + } + + /** + * 发送 POST JSON 请求,自动反序列化为 Map + */ + public Map postJson(String url, Map body, String token) throws IOException { + Request request = buildJsonRequest(url, body, token, "POST"); + return executeJson(request); + } + + /** + * 发送 GET 请求,自动反序列化为 Map + */ + public Map get(String url, String token) throws IOException { + Request request = buildJsonRequest(url, null, token, "GET"); + return executeJson(request); + } + + /** + * 流式 POST 请求,返回 Response 实例,调用方负责关闭 response.body().close() + */ + public Response postStream(String url, Map body, String token) throws IOException { + Request request = buildJsonRequest(url, body, token, "POST"); + return client.newCall(request).execute(); // 返回后调用方处理 body() + } + + /** + * 构建 JSON 类型的 Request 请求 + */ + private Request buildJsonRequest(String url, Map body, String token, String method) throws IOException { + RequestBody requestBody = null; + if (body != null) { + String json = objectMapper.writeValueAsString(body); + requestBody = RequestBody.create(json, MediaType.get("application/json; charset=utf-8")); + } + + Request.Builder builder = new Request.Builder().url(url); + if (token != null && !token.isBlank()) { + builder.header("Authorization", token); + } + + switch (method.toUpperCase()) { + case "POST": + builder.post(requestBody != null ? requestBody : RequestBody.create(new byte[0])); + break; + case "GET": + builder.get(); + break; + case "PUT": + builder.put(requestBody); + break; + case "DELETE": + if (requestBody != null) { + builder.delete(requestBody); + } else { + builder.delete(); + } + break; + default: + throw new IllegalArgumentException("不支持的请求方法:" + method); + } + + return builder.build(); + } + + /** + * 执行请求并解析为 Map + */ + private Map executeJson(Request request) throws IOException { + try (Response response = client.newCall(request).execute()) { + if (!response.isSuccessful()) { + throw new IOException("请求失败: " + response.code() + " - " + response.message()); + } + String json = response.body().string(); + return objectMapper.readValue(json, new TypeReference<>() {}); + } + } +} diff --git a/src/main/resources/application-dev-windows.properties b/src/main/resources/application-dev-windows.properties index 5340842..468702a 100644 --- a/src/main/resources/application-dev-windows.properties +++ b/src/main/resources/application-dev-windows.properties @@ -57,3 +57,5 @@ knowledge.base.redis.password= # 可留空 # Redis 默认缓存时间(分钟) knowledge.base.redis.default-expire-minutes=1440 +# 大模型相关 +llm.remote.base-url=http://localhost:3001 \ No newline at end of file diff --git a/src/main/resources/application-docker.properties b/src/main/resources/application-docker.properties index e81b4a5..8ffd97a 100644 --- a/src/main/resources/application-docker.properties +++ b/src/main/resources/application-docker.properties @@ -56,3 +56,6 @@ knowledge.base.redis.default-expire-minutes=1440 # Spring Boot Redis 自动配置(用于 RedisTemplate 和 Lettuce) spring.redis.host=host.docker.internal spring.redis.port=6379 + +# 大模型相关 +llm.remote.base-url=http://host.docker.internal:3001 \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index abc5ab2..96cfe2d 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -32,3 +32,6 @@ knowledge.base.redis.enable=false object.storage.use-uuid-prefix=true object.storage.public-buckets[0]=kbase object.storage.public-buckets[1]=public-media + +# 大模型相关 +llm.remote.base-url=http://llm.wisdompulse.cn \ No newline at end of file diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties index 8caa3b9..4fcff6c 100644 --- a/src/test/resources/application.properties +++ b/src/test/resources/application.properties @@ -22,5 +22,8 @@ import.schedule.enabled=true # 每小时执行一次(可改) import.schedule.cron=0 0 * * * * +# 大模型相关 +llm.remote.base-url=http://llm.wisdompulse.cn +