From 53467e8545a9ed38c18bad2ae7fdb673c5862ded Mon Sep 17 00:00:00 2001 From: luke Date: Mon, 7 Jul 2025 07:08:01 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0llm=E5=85=B6=E5=AE=83?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../south/llm/AnythingLLMServiceImpl.java | 187 +++++++++++++++++- .../infrastructure/south/llm/LLMService.java | 2 + .../base/infrastructure/util/HttpHelper.java | 59 ++++++ .../infrastructure/util/LocalFileUtil.java | 21 ++ .../south/llm/AnythingLLMServiceTest.java | 117 +++++++++++ src/test/resources/obsidian-test/demo.md | 21 ++ src/test/resources/obsidian-test/index.md | 39 ++++ 7 files changed, 442 insertions(+), 4 deletions(-) create mode 100644 src/test/java/com/knowledge/base/infrastructure/south/llm/AnythingLLMServiceTest.java create mode 100644 src/test/resources/obsidian-test/demo.md create mode 100644 src/test/resources/obsidian-test/index.md diff --git a/src/main/java/com/knowledge/base/infrastructure/south/llm/AnythingLLMServiceImpl.java b/src/main/java/com/knowledge/base/infrastructure/south/llm/AnythingLLMServiceImpl.java index b790a0f..72274ae 100644 --- a/src/main/java/com/knowledge/base/infrastructure/south/llm/AnythingLLMServiceImpl.java +++ b/src/main/java/com/knowledge/base/infrastructure/south/llm/AnythingLLMServiceImpl.java @@ -2,6 +2,9 @@ package com.knowledge.base.infrastructure.south.llm; import cn.hutool.core.collection.CollectionUtil; import cn.hutool.core.util.StrUtil; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.collect.Lists; import com.knowledge.base.infrastructure.cache.iface.UserCacheService; import com.knowledge.base.infrastructure.config.ConstantConfig; import com.knowledge.base.infrastructure.util.HttpHelper; @@ -14,12 +17,12 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; +import java.nio.file.Files; +import java.util.*; @Slf4j @Service @@ -90,4 +93,180 @@ public class AnythingLLMServiceImpl implements LLMService { userCacheService.cacheAnythingLLMSlugId(wsName, slug); return StrUtil.isBlank(slug) ? ConstantConfig.DEFAULT_SLUG_ID : slug; } + + + + public void importObsidianToLLM(String llmToken, List mdFiles, String vaultBasePath) { + String url = baseUrl + "/api/ext/obsidian/vault"; + + List> fileList = new ArrayList<>(); + for (File file : mdFiles) { + if (!file.getName().endsWith(".md")) continue; + try { + String content = new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8); + String relativePath = vaultBasePath == null ? file.getName() : + file.getAbsolutePath().replace(new File(vaultBasePath).getAbsolutePath(), "") + .replaceFirst("^[/\\\\]", ""); + + Map fileMap = new HashMap<>(); + fileMap.put("name", file.getName()); + fileMap.put("path", relativePath.replace(File.separator, "/")); + fileMap.put("content", content); + fileList.add(fileMap); + } catch (IOException e) { + log.warn("读取文件失败: {}", file.getAbsolutePath(), e); + } + } + + Map headers = new HashMap<>(); + headers.put("Authorization", "Bearer " + llmToken); + + Map body = new HashMap<>(); + body.put("files", fileList); + + try { + httpHelper.exchange( + url, + "POST", + headers, + body, + new TypeReference>() {} // 可忽略返回内容 + ); + } catch (IOException e) { + throw new RuntimeException("上传 Obsidian 文件失败", e); + } + } + + @Override + public Map fetchAttachments(String llmToken, String workspaceSlug) { + String url = baseUrl + "/api/workspace/" + workspaceSlug; + Map headers = new HashMap<>(); + headers.put("Authorization", "Bearer " + llmToken); + + try { + Map response = httpHelper.exchange( + url, + "GET", + headers, + null, + new TypeReference>() {} + ); + + Map result = new HashMap<>(); + if (response.containsKey("workspace")) { + Map workspace = (Map) response.get("workspace"); + Object documents = workspace.get("documents"); + if (documents instanceof Iterable) { + for (Object docObj : (Iterable) documents) { + if (docObj instanceof Map) { + Map doc = (Map) docObj; + String docpath = (String) doc.get("docpath"); + String metadataStr = (String) doc.get("metadata"); + if (docpath != null && metadataStr != null) { + try { + Map metadata = new ObjectMapper().readValue(metadataStr, new TypeReference>() {}); + String urlVal = (String) metadata.getOrDefault("url", ""); + result.put(docpath, urlVal); + } catch (Exception e) { + log.warn("解析 metadata 失败: {}", metadataStr, e); + } + } + } + } + } + } + return result; + } catch (IOException e) { + throw new RuntimeException("获取工作区附件失败: " + e.getMessage(), e); + } + } + + + public void updatePin(String llmToken, String workspaceSlug, String docPath, boolean pinStatus) { + String url = baseUrl + "/api/workspace/" + workspaceSlug + "/update-pin"; + Map headers = new HashMap<>(); + headers.put("Authorization", "Bearer " + llmToken); + + Map body = new HashMap<>(); + body.put("docPath", docPath); + body.put("pinStatus", pinStatus); + + try { + httpHelper.exchange( + url, + "POST", + headers, + body, + new TypeReference>() {} // 可忽略返回值,仅验证200 + ); + } catch (IOException e) { + throw new RuntimeException("设置 pin 状态失败: " + e.getMessage(), e); + } + } + + public List> getLocalFileItems(String llmToken, String workspaceSlug, String keyword) { + String url = baseUrl + "/api/system/local-files"; + + Map headers = new HashMap<>(); + headers.put("Authorization", "Bearer " + llmToken); + headers.put("Referer", baseUrl + "/workspace/" + workspaceSlug); + + try { + Map response = httpHelper.exchange( + url, + "GET", + headers, + null, + new TypeReference>() {} + ); + + // 从 localFiles -> items 遍历,查找 name 包含关键词的一级目录 + if (response.containsKey("localFiles")) { + Map localFiles = (Map) response.get("localFiles"); + Object items = localFiles.get("items"); + if (items instanceof List) { + for (Object folderObj : (List) items) { + if (folderObj instanceof Map) { + Map folder = (Map) folderObj; + String name = (String) folder.get("name"); + if (name != null && name.contains(keyword)) { + Object matchedItems = folder.get("items"); + if (matchedItems instanceof List) { + return (List>) matchedItems; + } + } + } + } + } + } + return Lists.newArrayList(); + } catch (IOException e) { + throw new RuntimeException("获取本地文件失败:" + e.getMessage(), e); + } + } + + + public void updateEmbeddings(String llmToken, String workspaceSlug, List adds, List deletes) { + String url = baseUrl + "/api/workspace/" + workspaceSlug + "/update-embeddings"; + + Map headers = new HashMap<>(); + headers.put("Authorization", "Bearer " + llmToken); + + Map body = new HashMap<>(); + body.put("adds", adds); + body.put("deletes", deletes); + + try { + httpHelper.exchange( + url, + "POST", + headers, + body, + new TypeReference>() {} // 可忽略返回,仅校验状态 + ); + } catch (IOException e) { + throw new RuntimeException("调用 update-embeddings 失败: " + e.getMessage(), e); + } + } + } diff --git a/src/main/java/com/knowledge/base/infrastructure/south/llm/LLMService.java b/src/main/java/com/knowledge/base/infrastructure/south/llm/LLMService.java index 49e19cf..8706f71 100644 --- a/src/main/java/com/knowledge/base/infrastructure/south/llm/LLMService.java +++ b/src/main/java/com/knowledge/base/infrastructure/south/llm/LLMService.java @@ -10,5 +10,7 @@ public interface LLMService { void streamAnswer(String llmToken, String question, Map params, WriterAdapter writer) throws Exception; boolean supports(String type); + + Map fetchAttachments(String llmToken, String workspaceSlug); } diff --git a/src/main/java/com/knowledge/base/infrastructure/util/HttpHelper.java b/src/main/java/com/knowledge/base/infrastructure/util/HttpHelper.java index 1b1ce9a..55291f2 100644 --- a/src/main/java/com/knowledge/base/infrastructure/util/HttpHelper.java +++ b/src/main/java/com/knowledge/base/infrastructure/util/HttpHelper.java @@ -101,6 +101,65 @@ public class HttpHelper { return builder.build(); } + /** + * 通用 HTTP 请求方法,支持任意 method、headers、请求体,自动反序列化返回 + * + * @param url 请求地址 + * @param method 请求方法,如 GET、POST、PUT、DELETE + * @param headers 请求头(可选,可为 null) + * @param body 请求体对象(可选,可为 null) + * @param responseType 返回类型(例如 Map.class) + * @param 泛型类型 + * @return 反序列化后的对象 + * @throws IOException 请求失败或 JSON 解析异常 + */ + /** + * 通用 HTTP 请求方法,支持任意 method、headers、请求体,支持泛型 TypeReference 返回(Jackson) + */ + public T exchange(String url, String method, Map headers, Object body, TypeReference typeRef) 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 (headers != null) { + for (Map.Entry entry : headers.entrySet()) { + builder.addHeader(entry.getKey(), entry.getValue()); + } + } + + switch (method.toUpperCase()) { + case "POST": + builder.post(requestBody != null ? requestBody : RequestBody.create(new byte[0])); + break; + case "PUT": + builder.put(requestBody != null ? requestBody : RequestBody.create(new byte[0])); + break; + case "DELETE": + builder.delete(requestBody != null ? requestBody : null); + break; + case "GET": + default: + builder.get(); + break; + } + + Request request = builder.build(); + + 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, typeRef); + } + } + + + /** * 执行请求并解析为 Map */ diff --git a/src/main/java/com/knowledge/base/infrastructure/util/LocalFileUtil.java b/src/main/java/com/knowledge/base/infrastructure/util/LocalFileUtil.java index 1310843..e20fb34 100644 --- a/src/main/java/com/knowledge/base/infrastructure/util/LocalFileUtil.java +++ b/src/main/java/com/knowledge/base/infrastructure/util/LocalFileUtil.java @@ -2,8 +2,10 @@ package com.knowledge.base.infrastructure.util; import lombok.extern.slf4j.Slf4j; +import java.io.File; import java.io.IOException; import java.nio.file.*; +import java.util.ArrayList; import java.util.List; @@ -71,5 +73,24 @@ public class LocalFileUtil { return targetFilePath; } + + + public static List listMarkdownFiles(File root) { + List result = new ArrayList<>(); + if (root == null || !root.exists()) return result; + + File[] files = root.listFiles(); + if (files == null) return result; + + for (File file : files) { + if (file.isDirectory()) { + result.addAll(listMarkdownFiles(file)); + } else if (file.getName().endsWith(".md")) { + result.add(file); + } + } + return result; + } + } diff --git a/src/test/java/com/knowledge/base/infrastructure/south/llm/AnythingLLMServiceTest.java b/src/test/java/com/knowledge/base/infrastructure/south/llm/AnythingLLMServiceTest.java new file mode 100644 index 0000000..e58abb7 --- /dev/null +++ b/src/test/java/com/knowledge/base/infrastructure/south/llm/AnythingLLMServiceTest.java @@ -0,0 +1,117 @@ +package com.knowledge.base.infrastructure.south.llm; + +import com.knowledge.base.infrastructure.cache.iface.UserCacheService; +import com.knowledge.base.infrastructure.util.HttpHelper; +import com.knowledge.base.infrastructure.util.http.WriterAdapter; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import java.io.File; +import java.util.*; + +/** + * @author Luke.ye + * @date 2025/7/7 06:57 + */ +@Slf4j +@SpringBootTest(properties = { + "spring.profiles.active=dev-mac" +}) +public class AnythingLLMServiceTest { + + @Autowired + private AnythingLLMServiceImpl llmService; + + @Autowired + private UserCacheService userCacheService; + + @Autowired + private HttpHelper httpHelper; + + private String token; + + @BeforeEach + public void setUp() { + try { + this.token = llmService.fetchToken("lukeye@6"); + } catch (Exception e) { + log.warn("获取 token 失败: {}", e.getMessage(), e); + } + } + + @Test + public void testImportObsidianToLLM() { + try { + File file1 = new File("src/test/resources/obsidian-test/index.md"); + File file2 = new File("src/test/resources/obsidian-test/demo.md"); + + llmService.importObsidianToLLM(token, Arrays.asList(file1, file2), "src/test/resources/obsidian-test"); + } catch (Exception e) { + log.warn("测试 importObsidianToLLM 异常", e); + } + } + + @Test + public void testUpdateEmbeddings() { + try { + String slug = "your-workspace-slug"; + List adds = Collections.singletonList("public/xx.md"); + List deletes = Arrays.asList(); + + llmService.updateEmbeddings(token, slug, adds, deletes); + } catch (Exception e) { + log.warn("测试 updateEmbeddings 异常", e); + } + } + + @Test + public void testUpdatePin() { + try { + String slug = "your-workspace-slug"; + String path = "public/xx.md"; + + llmService.updatePin(token, slug, path, true); + } catch (Exception e) { + log.warn("测试 updatePin 异常", e); + } + } + + @Test + public void testFetchAttachments() { + try { + String slug = "your-workspace-slug"; + Map attachments = llmService.fetchAttachments(token, slug); + attachments.forEach((k, v) -> log.info("Attachment: {} -> {}", k, v)); + } catch (Exception e) { + log.warn("测试 fetchAttachments 异常", e); + } + } + + @Test + public void testGetLocalFileItems() { + try { + String slug = "your-workspace-slug"; + List> result = llmService.getLocalFileItems(token, slug, "导出"); + log.info("本地文件项:{}", result); + } catch (Exception e) { + log.warn("测试 getLocalFileItems 异常", e); + } + } + + @Test + public void testStreamAnswer() { + try { + WriterAdapter writer = line -> log.info("回答流:{}", line); + + Map params = new HashMap<>(); + params.put("wsName", "部门知识库"); + llmService.streamAnswer(token, "介绍一下黄金圈法则", params, writer); + } catch (Exception e) { + log.warn("测试 streamAnswer 异常", e); + } + } +} + diff --git a/src/test/resources/obsidian-test/demo.md b/src/test/resources/obsidian-test/demo.md new file mode 100644 index 0000000..b1668b6 --- /dev/null +++ b/src/test/resources/obsidian-test/demo.md @@ -0,0 +1,21 @@ + +修订记录 + +| 版本 | 时间 | 修订人 | 改动点 | +|:-----:|:----------:|:-------:|:---------------------------------:| +| 1.0.0 | 2025-06-05 | Luke.Ye | 添加修订记录(新增token校验接口) | +| 1.0.1 | 2025-06-06 | Luke.Ye | 重构登录Token校验逻辑, | +| 1.0.2 | 2025-06-09 | Luke.Ye | 添加用户角色相关逻辑,重构代码 | +| 1.0.3 | 2025-06-16 | Luke.Ye | 新增文档上传至对象存储接口 | +| 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.0.7 | 2025-06-22 | Luke.Ye | 导入文档类型新增支持ppt & txt | +| 1.1.0 | 2025-06-22 | Luke.Ye | 完成llm接口迁移至后端 | +| 1.1.1 | 2025-06-26 | Luke.Ye | 提供删除ES数据的接口(ob仓库监听发生删除事件,则同步删除ES) | + + + +## 开发顺序 + +PO ---> Mapper ---> iface ---> persistence \ No newline at end of file diff --git a/src/test/resources/obsidian-test/index.md b/src/test/resources/obsidian-test/index.md new file mode 100644 index 0000000..6d04825 --- /dev/null +++ b/src/test/resources/obsidian-test/index.md @@ -0,0 +1,39 @@ + + +### 提示词权重 +提示词权重,多个可以级联,权重可以低,但不能过高 +- `[]`:0.9,最多可以套3层,最多0.729倍 +- `()`:1.1,最多可以套3层,最多1.331倍。也可以直接将*权重(范围需要控制在0.3~1.5)* 放在提示词后面,例如:`(garden:1.5)`, +- `{}`:1.05,最多可以套3层,最多1.15倍 +- `<>`:调用Lora, + + +### 提示词顺序 +越靠前,权重越大 + +No1:画质词/画风词,比如 `4k, masterpiece,high quality, highly detailed` +No2:主体,比如 `1girl, blue dress` +No3:环境/场景/构图,比如 `garden, white background` +No4:lora + +### 提示词污染 & 融合 +污染:不同的提示词(尤其是颜色)会相互渗透,要使用break来隔开提示词, + +融合:`1gril AND cat` 或者 `1girl_cat` 或者 `[1gril|cat]` ,猫女 + +### 画的时间 + +`{1girl:garden:0.7}`:70%的时间画girl,后面30%画花园 + +### 起手式 + +正向:4k、8k、masterpiece + +反向:blur(模糊) + + + + + +## 图生图 +重绘幅度:建议 `0.4~0.7` \ No newline at end of file