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
+