opt
This commit is contained in:
parent
a222e64d24
commit
45537ab339
@ -33,7 +33,7 @@ public class AuthFilter implements Filter {
|
||||
String path = request.getRequestURI();
|
||||
|
||||
// 需要鉴权的路径前缀
|
||||
if (path.startsWith("/api/v1/doc")) {
|
||||
if (path.startsWith("/api/v1/doc") || path.startsWith("/api/v1/llm")) {
|
||||
String token = request.getHeader("Authorization");
|
||||
|
||||
if (StrUtil.isBlank(token)) {
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
package com.knowledge.base.application.service;
|
||||
|
||||
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitter;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public interface LLMAppService {
|
||||
String getToken(String password) throws Exception;
|
||||
ResponseBodyEmitter ask(String userId, String question, Map<String, Object> params) throws Exception;
|
||||
SseEmitter ask(String llmToken, String question, Map<String, Object> params) throws Exception;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -1,17 +1,18 @@
|
||||
package com.knowledge.base.application.service;
|
||||
|
||||
import com.knowledge.base.infrastructure.south.llm.LLMService;
|
||||
import com.knowledge.base.infrastructure.south.llm.LLMServiceFactory;
|
||||
import com.knowledge.base.infrastructure.util.http.SseOutputAdapter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitter;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
|
||||
import java.io.PipedInputStream;
|
||||
import java.io.PipedOutputStream;
|
||||
import java.io.*;
|
||||
import java.util.Map;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class LLMAppServiceImpl implements LLMAppService {
|
||||
|
||||
private final LLMServiceFactory llmServiceFactory;
|
||||
@ -22,33 +23,19 @@ public class LLMAppServiceImpl implements LLMAppService {
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseBodyEmitter ask(String userId, String question, Map<String, Object> params) throws Exception {
|
||||
LLMService service = llmServiceFactory.current();
|
||||
|
||||
ResponseBodyEmitter emitter = new ResponseBodyEmitter();
|
||||
PipedOutputStream out = new PipedOutputStream();
|
||||
PipedInputStream in = new PipedInputStream(out);
|
||||
public SseEmitter ask(String llmToken, String question, Map<String, Object> params) throws Exception {
|
||||
SseEmitter emitter = new SseEmitter(0L); // 不超时
|
||||
|
||||
new Thread(() -> {
|
||||
try {
|
||||
service.streamAnswer(userId, question, params, out);
|
||||
} catch (Exception e) {
|
||||
try {
|
||||
emitter.send("data: {\"error\": \"LLM内部异常\"}\n\n");
|
||||
} catch (Exception ignore) {}
|
||||
emitter.completeWithError(e);
|
||||
}
|
||||
}).start();
|
||||
|
||||
new Thread(() -> {
|
||||
try (in) {
|
||||
byte[] buf = new byte[1024];
|
||||
int len;
|
||||
while ((len = in.read(buf)) != -1) {
|
||||
emitter.send(new String(buf, 0, len));
|
||||
}
|
||||
llmServiceFactory.current().streamAnswer(llmToken, question, params, new SseOutputAdapter(emitter));
|
||||
emitter.complete();
|
||||
} catch (Exception e) {
|
||||
try {
|
||||
emitter.send(SseEmitter.event().data("{\"error\": \"LLM异常\"}"));
|
||||
} catch (IOException ignored) {
|
||||
log.error("error", e);
|
||||
}
|
||||
emitter.completeWithError(e);
|
||||
}
|
||||
}).start();
|
||||
|
||||
@ -22,4 +22,9 @@ public interface UserCacheService {
|
||||
* @param userJsonStr
|
||||
*/
|
||||
void cacheUserJsonByToken(String token, String userJsonStr);
|
||||
|
||||
|
||||
default void cacheAnythingLLMSlugId(String wsName, String slugId) {}
|
||||
|
||||
default Optional<String> getAnythingLLMSlugIdByWorkspaceName(String wsName) {return Optional.empty();}
|
||||
}
|
||||
|
||||
@ -8,6 +8,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@ -39,4 +40,14 @@ public class RedisUserCacheServiceImpl implements UserCacheService {
|
||||
public void cacheUserJsonByToken(String token, String userJsonStr) {
|
||||
redisTemplate.opsForValue().set(key("token", token), userJsonStr, ConstantConfig.USER_CACHE_EXPIRED_MINUTES, TimeUnit.MINUTES);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cacheAnythingLLMSlugId(String wsName, String slugId) {
|
||||
redisTemplate.opsForValue().set(key("ws-name", wsName), slugId, Duration.ofDays(10).toMinutes(), TimeUnit.MINUTES);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<String> getAnythingLLMSlugIdByWorkspaceName(String wsName) {
|
||||
return Optional.ofNullable(redisTemplate.opsForValue().get(key("ws-name", wsName)));
|
||||
}
|
||||
}
|
||||
|
||||
@ -28,4 +28,12 @@ public class ConstantConfig {
|
||||
public static final String DEFAULT_UPLOADER = "ADMIN";
|
||||
|
||||
public static final String UNKNOWN_LOCAL_RELA_PATH = "UNKNOWN";
|
||||
|
||||
|
||||
/**
|
||||
* 以下是AnythingLLM相关
|
||||
*/
|
||||
public static final String DEFAULT_SLUG_ID = "87e14982-a821-48d8-9c6b-3557d0bb2f96";
|
||||
|
||||
|
||||
}
|
||||
|
||||
@ -4,7 +4,7 @@ import com.knowledge.base.application.service.LLMAppService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitter;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@ -24,11 +24,11 @@ public class LLMController {
|
||||
}
|
||||
|
||||
@PostMapping(value = "/ask", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
|
||||
public ResponseBodyEmitter ask(@RequestHeader("Authorization") String token,
|
||||
@RequestBody Map<String, Object> body) throws Exception {
|
||||
public SseEmitter ask(@RequestHeader("Authorization") String token,
|
||||
@RequestBody Map<String, Object> body) throws Exception {
|
||||
String question = String.valueOf(body.get("question"));
|
||||
String userId = String.valueOf(body.getOrDefault("userId", "anonymous"));
|
||||
return llmAppService.ask(userId, question, body);
|
||||
String llmToken = String.valueOf(body.getOrDefault("llmToken", "anonymous"));
|
||||
return llmAppService.ask(llmToken, question, body);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,8 +1,13 @@
|
||||
package com.knowledge.base.infrastructure.south.llm;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import cn.hutool.core.collection.CollectionUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.knowledge.base.infrastructure.cache.iface.UserCacheService;
|
||||
import com.knowledge.base.infrastructure.config.ConstantConfig;
|
||||
import com.knowledge.base.infrastructure.util.HttpHelper;
|
||||
import com.knowledge.base.infrastructure.util.http.WriterAdapter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import okhttp3.Response;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
@ -12,8 +17,10 @@ import java.io.BufferedReader;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@ -24,11 +31,7 @@ public class AnythingLLMServiceImpl implements LLMService {
|
||||
private String baseUrl;
|
||||
|
||||
private final HttpHelper httpHelper;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
private String cachedToken;
|
||||
private long cachedAt;
|
||||
private final long cacheExpireMs = 60 * 60 * 1000;
|
||||
private final UserCacheService userCacheService;
|
||||
|
||||
@Override
|
||||
public boolean supports(String type) {
|
||||
@ -37,24 +40,18 @@ public class AnythingLLMServiceImpl implements LLMService {
|
||||
|
||||
@Override
|
||||
public String fetchToken(String password) throws Exception {
|
||||
String url = baseUrl + "/api/token";
|
||||
String url = baseUrl + "/api/request-token";
|
||||
Map<String, Object> result = httpHelper.postJson(url, Map.of("password", password), null);
|
||||
String token = String.valueOf(result.get("token"));
|
||||
cachedToken = token;
|
||||
cachedAt = System.currentTimeMillis();
|
||||
return token;
|
||||
}
|
||||
|
||||
private String getValidToken() throws Exception {
|
||||
if (cachedToken != null && (System.currentTimeMillis() - cachedAt) < cacheExpireMs) {
|
||||
return cachedToken;
|
||||
}
|
||||
return fetchToken("wisdom2025");
|
||||
return String.valueOf(result.get("token"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void streamAnswer(String userId, String question, Map<String, Object> params, OutputStream output) throws Exception {
|
||||
String slug = String.valueOf(params.getOrDefault("slug", "kb"));
|
||||
public void streamAnswer(String llmToken, String question, Map<String, Object> params, WriterAdapter writer) throws Exception {
|
||||
String slug = (String) params.get("slug");
|
||||
if (StrUtil.isBlank(slug)) {
|
||||
slug = fetchSlugByWsName(llmToken, (String) params.getOrDefault("wsName", "部门知识库"));
|
||||
}
|
||||
|
||||
String url = baseUrl + "/api/workspace/" + slug + "/stream-chat";
|
||||
|
||||
Map<String, Object> body = Map.of(
|
||||
@ -62,14 +59,36 @@ public class AnythingLLMServiceImpl implements LLMService {
|
||||
"attachments", params.getOrDefault("attachments", List.of())
|
||||
);
|
||||
|
||||
try (Response response = httpHelper.postStream(url, body, "Bearer " + getValidToken());
|
||||
try (Response response = httpHelper.postStream(url, body, "Bearer " + llmToken);
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(response.body().byteStream(), StandardCharsets.UTF_8))) {
|
||||
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
output.write((line + "\n").getBytes(StandardCharsets.UTF_8));
|
||||
output.flush();
|
||||
writer.writeLine(line); // 已是 SSE 格式
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
private String fetchSlugByWsName(String llmToken, String wsName) {
|
||||
Optional<String> slugId = userCacheService.getAnythingLLMSlugIdByWorkspaceName(wsName);
|
||||
if(slugId.isPresent()) {
|
||||
return slugId.get();
|
||||
}
|
||||
String url = baseUrl + "/api/workspaces";
|
||||
Map<String, Object> resMap = httpHelper.get(url, llmToken);
|
||||
List<HashMap> workspaces = (List<HashMap>)resMap.get("workspaces");
|
||||
if(CollectionUtil.isEmpty(workspaces)) {
|
||||
return ConstantConfig.DEFAULT_SLUG_ID;
|
||||
}
|
||||
Optional<HashMap> targetWorkspace = workspaces.stream().filter(workspace -> workspace.get("name").equals(wsName)).findFirst();
|
||||
if(targetWorkspace.isEmpty()) {
|
||||
return ConstantConfig.DEFAULT_SLUG_ID;
|
||||
}
|
||||
|
||||
String slug = (String) targetWorkspace.get().get("slug");
|
||||
// slug数据进缓存
|
||||
userCacheService.cacheAnythingLLMSlugId(wsName, slug);
|
||||
return StrUtil.isBlank(slug) ? ConstantConfig.DEFAULT_SLUG_ID : slug;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,12 +1,13 @@
|
||||
package com.knowledge.base.infrastructure.south.llm;
|
||||
|
||||
import java.io.OutputStream;
|
||||
import com.knowledge.base.infrastructure.util.http.WriterAdapter;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public interface LLMService {
|
||||
String fetchToken(String password) throws Exception;
|
||||
|
||||
void streamAnswer(String userId, String question, Map<String, Object> params, OutputStream output) throws Exception;
|
||||
void streamAnswer(String llmToken, String question, Map<String, Object> params, WriterAdapter writer) throws Exception;
|
||||
|
||||
boolean supports(String type);
|
||||
}
|
||||
|
||||
@ -22,8 +22,8 @@ public class HttpHelper {
|
||||
|
||||
public HttpHelper() {
|
||||
this.client = new OkHttpClient.Builder()
|
||||
.connectTimeout(20, TimeUnit.SECONDS)
|
||||
.readTimeout(30, TimeUnit.SECONDS)
|
||||
.connectTimeout(60, TimeUnit.SECONDS)
|
||||
.readTimeout(300, TimeUnit.SECONDS)
|
||||
.build();
|
||||
this.objectMapper = new ObjectMapper();
|
||||
}
|
||||
@ -48,8 +48,18 @@ public class HttpHelper {
|
||||
* 流式 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()
|
||||
String json = objectMapper.writeValueAsString(body);
|
||||
RequestBody requestBody = RequestBody.create(json, MediaType.get("application/json; charset=utf-8"));
|
||||
|
||||
Request request = new Request.Builder()
|
||||
.url(url)
|
||||
.addHeader("Authorization", token)
|
||||
.addHeader("Accept", "text/event-stream")
|
||||
.addHeader("Connection", "keep-alive")
|
||||
.post(requestBody)
|
||||
.build();
|
||||
|
||||
return client.newCall(request).execute();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -0,0 +1,22 @@
|
||||
package com.knowledge.base.infrastructure.util.http;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class SseOutputAdapter implements WriterAdapter {
|
||||
|
||||
private final SseEmitter emitter;
|
||||
|
||||
public SseOutputAdapter(SseEmitter emitter) {
|
||||
this.emitter = emitter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeLine(String line) throws IOException {
|
||||
if (StrUtil.isNotBlank(line)) {
|
||||
emitter.send(SseEmitter.event().data(line));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,8 @@
|
||||
package com.knowledge.base.infrastructure.util.http;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public interface WriterAdapter {
|
||||
void writeLine(String line) throws IOException;
|
||||
}
|
||||
|
||||
@ -59,3 +59,4 @@ knowledge.base.redis.default-expire-minutes=1440
|
||||
|
||||
# 大模型相关
|
||||
llm.remote.base-url=http://localhost:3001
|
||||
#llm.remote.base-url=http://llm.wisdompulse.cn
|
||||
Loading…
x
Reference in New Issue
Block a user