完成llm接口迁移至后端

This commit is contained in:
luke 2025-06-22 08:32:51 +08:00
parent ff8a2c5089
commit 647ef43655
9 changed files with 252 additions and 1 deletions

View File

@ -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接口迁移至后端 |

19
pom.xml
View File

@ -47,6 +47,7 @@
<org.bouncycastle.version>1.70</org.bouncycastle.version>
<minio.version>8.5.3</minio.version>
<okhttp.version>4.12.0</okhttp.version>
</properties>
<dependencyManagement>
<dependencies>
@ -225,6 +226,19 @@
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>${minio.version}</version>
<exclusions>
<exclusion>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>${okhttp.version}</version>
</dependency>
@ -381,6 +395,11 @@
<artifactId>minio</artifactId>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
</dependency>
</dependencies>
<build>

View File

@ -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;
}

View File

@ -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<String, Object> getToken(@RequestBody Map<String, Object> 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<String, Object> 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<String, Object> 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;
}
}

View File

@ -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 POSTGETToken注入泛型反序列化流式请求
*/
@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<String, Object> postJson(String url, Map<String, Object> body, String token) throws IOException {
Request request = buildJsonRequest(url, body, token, "POST");
return executeJson(request);
}
/**
* 发送 GET 请求自动反序列化为 Map
*/
public Map<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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<>() {});
}
}
}

View File

@ -57,3 +57,5 @@ knowledge.base.redis.password= # 可留空
# Redis 默认缓存时间(分钟)
knowledge.base.redis.default-expire-minutes=1440
# 大模型相关
llm.remote.base-url=http://localhost:3001

View File

@ -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

View File

@ -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

View File

@ -22,5 +22,8 @@ import.schedule.enabled=true
# 每小时执行一次(可改)
import.schedule.cron=0 0 * * * *
# 大模型相关
llm.remote.base-url=http://llm.wisdompulse.cn