From 37b696e61b6041b781f38caae41bae90f2a38b42 Mon Sep 17 00:00:00 2001 From: zhangsan <646228430@qq.com> Date: Tue, 29 Jul 2025 13:18:59 +0800 Subject: [PATCH] =?UTF-8?q?7.29=20=E5=A2=9E=E5=8A=A0=E5=88=A0=E9=99=A4?= =?UTF-8?q?=E7=9F=A5=E8=AF=86=E5=BA=93=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 + .../main/java/edu/whut/api/IAiService.java | 3 +- .../java/edu/whut/config/OllamaConfig.java | 70 ++--- .../src/test/java/edu/whut/test/JGitTest.java | 102 ++----- .../src/test/java/edu/whut/test/RAGTest.java | 106 +++---- .../whut/trigger/http/OllamaController.java | 70 ++--- .../edu/whut/trigger/http/RAGController.java | 113 +++++--- docs/rag-dev-ops/nginx/html/git.html | 2 +- docs/rag-dev-ops/nginx/html/upload.html | 2 +- docs/tag/v1.0/api/curl.sh | 10 +- docs/tag/v1.0/log/log-info-2025-07-28.0.log | 20 -- docs/tag/v1.0/log/log_error.log | 0 docs/tag/v1.0/log/log_info.log | 229 --------------- docs/tag/v1.0/nginx/html/index.html | 25 +- docs/tag/v1.0/nginx/html/js/index.js | 267 ++++++++++++++---- pom.xml | 2 +- 16 files changed, 459 insertions(+), 564 deletions(-) delete mode 100644 docs/tag/v1.0/log/log-info-2025-07-28.0.log delete mode 100644 docs/tag/v1.0/log/log_error.log delete mode 100644 docs/tag/v1.0/log/log_info.log diff --git a/.gitignore b/.gitignore index 3daf4fd..9cb64c5 100644 --- a/.gitignore +++ b/.gitignore @@ -47,3 +47,5 @@ build/ /docs/tag/v1.0/ollama/models/ /docs/tag/v1.0/ollama/id_ed25519 /docs/tag/v1.0/ollama/id_ed25519.pub +/docs/tag/v1.0/log/ +/docs/tag/v1.0/log/ diff --git a/ai-rag-knowledge-api/src/main/java/edu/whut/api/IAiService.java b/ai-rag-knowledge-api/src/main/java/edu/whut/api/IAiService.java index b2806b8..e3829c8 100644 --- a/ai-rag-knowledge-api/src/main/java/edu/whut/api/IAiService.java +++ b/ai-rag-knowledge-api/src/main/java/edu/whut/api/IAiService.java @@ -1,6 +1,6 @@ package edu.whut.api; -import org.springframework.ai.chat.ChatResponse; +import org.springframework.ai.chat.model.ChatResponse; // ← 注意新包 import reactor.core.publisher.Flux; public interface IAiService { @@ -10,5 +10,4 @@ public interface IAiService { Flux generateStream(String model, String message); Flux generateStreamRag(String model, String ragTag, String message); - } diff --git a/ai-rag-knowledge-app/src/main/java/edu/whut/config/OllamaConfig.java b/ai-rag-knowledge-app/src/main/java/edu/whut/config/OllamaConfig.java index 4f7d74d..234d560 100644 --- a/ai-rag-knowledge-app/src/main/java/edu/whut/config/OllamaConfig.java +++ b/ai-rag-knowledge-app/src/main/java/edu/whut/config/OllamaConfig.java @@ -1,19 +1,25 @@ package edu.whut.config; -import org.springframework.ai.ollama.OllamaChatClient; -import org.springframework.ai.ollama.OllamaEmbeddingClient; +import org.springframework.ai.embedding.EmbeddingModel; +import org.springframework.ai.ollama.OllamaChatModel; +import org.springframework.ai.ollama.OllamaEmbeddingModel; import org.springframework.ai.ollama.api.OllamaApi; import org.springframework.ai.ollama.api.OllamaOptions; import org.springframework.ai.transformer.splitter.TokenTextSplitter; -import org.springframework.ai.vectorstore.PgVectorStore; import org.springframework.ai.vectorstore.SimpleVectorStore; +import org.springframework.ai.vectorstore.pgvector.PgVectorStore; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; import org.springframework.jdbc.core.JdbcTemplate; /** - * 配置 Ollama 和 OpenAI 客户端,以及向量存储和文本拆分器等 Bean + * 配置 Ollama 客户端、Embedding 模型、向量存储和文本拆分器等 Bean + * 注意(1.0.0-M6): + * - 不再使用 OllamaChatClient,改为 OllamaChatModel + * - Embedding 使用 EmbeddingModel 接口 + OllamaEmbeddingModel + * - PgVectorStore 与 SimpleVectorStore 使用 builder 方式创建 */ @Configuration public class OllamaConfig { @@ -22,22 +28,23 @@ public class OllamaConfig { * 创建 Ollama API 客户端,负责与 Ollama 服务的基础通信 */ @Bean - public OllamaApi ollamaApi( - @Value("${spring.ai.ollama.base-url}") String baseUrl) { + public OllamaApi ollamaApi(@Value("${spring.ai.ollama.base-url}") String baseUrl) { return new OllamaApi(baseUrl); } - /** - * 基于 OllamaApi 实例创建对话客户端,用于与 Ollama 模型进行对话交互 + * 对话模型;控制器里通过 OllamaOptions 指定具体 chat 模型(如 deepseek-r1:1.5b) */ @Bean - public OllamaChatClient ollamaChatClient(OllamaApi ollamaApi) { - return new OllamaChatClient(ollamaApi); + public OllamaChatModel ollamaChatModel(OllamaApi ollamaApi) { + // 如需设置默认对话模型,可在这里 .defaultOptions(OllamaOptions.builder().model("xxx").build()) + return OllamaChatModel.builder() + .ollamaApi(ollamaApi) + .build(); } /** - * 文本拆分器,根据 Token 划分长文本,便于分段处理或嵌入计算 + * 文本拆分器:基于 Token 切分 */ @Bean public TokenTextSplitter tokenTextSplitter() { @@ -45,36 +52,33 @@ public class OllamaConfig { } /** - * 创建一个简单的向量存储 (in-memory),可根据配置选择 Ollama 或 OpenAI 的嵌入模型 + * EmbeddingModel(Ollama):使用配置项 spring.ai.rag.embed 指定 embedding 模型,如 nomic-embed-text */ @Bean - public SimpleVectorStore vectorStore( - @Value("${spring.ai.rag.embed}") String model, + @Primary + public EmbeddingModel embeddingModel( + @Value("${spring.ai.rag.embed}") String embedModel, OllamaApi ollamaApi) { - - // 固定使用 Ollama 的嵌入客户端(不再走 OpenAI 分支) - OllamaEmbeddingClient embeddingClient = new OllamaEmbeddingClient(ollamaApi); - embeddingClient.withDefaultOptions( - OllamaOptions.create().withModel(model) - ); - return new SimpleVectorStore(embeddingClient); + return OllamaEmbeddingModel.builder() + .ollamaApi(ollamaApi) + .defaultOptions(OllamaOptions.builder().model(embedModel).build()) + .build(); } /** - * 创建基于 PostgreSQL pgvector 扩展的持久化向量存储,可根据配置选择 Ollama 或 OpenAI 模型 + * 简单内存向量存储(in-memory) */ @Bean - public PgVectorStore pgVectorStore( - @Value("${spring.ai.rag.embed}") String model, - OllamaApi ollamaApi, - JdbcTemplate jdbcTemplate) { - - // 固定使用 Ollama 的嵌入客户端(不再走 OpenAI 分支) - OllamaEmbeddingClient embeddingClient = new OllamaEmbeddingClient(ollamaApi); - embeddingClient.withDefaultOptions( - OllamaOptions.create().withModel(model) - ); - return new PgVectorStore(jdbcTemplate, embeddingClient); + public SimpleVectorStore vectorStore(EmbeddingModel embeddingModel) { + return SimpleVectorStore.builder(embeddingModel).build(); } + /** + * 基于 PostgreSQL pgvector 的持久化向量存储 + * 如需自定义表名可追加 .vectorTableName("your_table") + */ + @Bean + public PgVectorStore pgVectorStore(JdbcTemplate jdbcTemplate, EmbeddingModel embeddingModel) { + return PgVectorStore.builder(jdbcTemplate, embeddingModel).build(); + } } diff --git a/ai-rag-knowledge-app/src/test/java/edu/whut/test/JGitTest.java b/ai-rag-knowledge-app/src/test/java/edu/whut/test/JGitTest.java index 27c27d1..f72fc64 100644 --- a/ai-rag-knowledge-app/src/test/java/edu/whut/test/JGitTest.java +++ b/ai-rag-knowledge-app/src/test/java/edu/whut/test/JGitTest.java @@ -1,3 +1,4 @@ +// src/test/java/edu/whut/test/JGitTest.java package edu.whut.test; import jakarta.annotation.Resource; @@ -7,22 +8,20 @@ import org.eclipse.jgit.api.Git; import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider; import org.junit.Test; import org.junit.runner.RunWith; -import org.springframework.ai.document.Document; -import org.springframework.ai.ollama.OllamaChatClient; -import org.springframework.ai.reader.tika.TikaDocumentReader; -import org.springframework.ai.transformer.splitter.TokenTextSplitter; -import org.springframework.ai.vectorstore.PgVectorStore; -import org.springframework.ai.vectorstore.SimpleVectorStore; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.core.io.PathResource; import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.ai.document.Document; +import org.springframework.ai.reader.tika.TikaDocumentReader; +import org.springframework.ai.transformer.splitter.TokenTextSplitter; +import org.springframework.ai.vectorstore.SimpleVectorStore; +import org.springframework.ai.vectorstore.pgvector.PgVectorStore; +import org.springframework.ai.ollama.OllamaChatModel; import java.io.File; -import java.io.IOException; import java.nio.file.*; import java.nio.file.attribute.BasicFileAttributes; import java.util.ArrayList; -import java.util.Collections; import java.util.List; /** @@ -33,116 +32,69 @@ import java.util.List; @SpringBootTest public class JGitTest { - /** - * Ollama 聊天客户端,用于后续可能的模型调用(本例未使用)。 - */ + /** Ollama 对话模型(本例未使用,仅演示注入) */ @Resource - private OllamaChatClient ollamaChatClient; + private OllamaChatModel ollamaChatModel; - /** - * 文本拆分器:将长文档拆分成多个小段。 - */ + /** 文本拆分器:将长文档拆分成多个小段 */ @Resource private TokenTextSplitter tokenTextSplitter; - /** - * 简单的内存向量存储,用于快速测试(本例未使用)。 - */ + /** 简单内存向量存储(本例未使用,仅演示注入) */ @Resource private SimpleVectorStore simpleVectorStore; - /** - * PostgreSQL 向量存储,用于持久化文档向量。 - */ + /** PostgreSQL pgvector 存储,用于持久化文档向量 */ @Resource private PgVectorStore pgVectorStore; - /** - * 测试方法:克隆远程 Git 仓库到本地目录。 - * @throws Exception 在克隆过程出错时抛出 - */ + /** 克隆远程 Git 仓库到本地 */ @Test - public void test() throws Exception { - // 远程仓库地址 - String repoURL = "http://124.71.159.195:3000/zy123/group-buying.git"; - // 认证用户名和密码,如果仓库是私有的需要提供 - String username = "zy123xxxx"; - String password = "xxxxxxx"; - - // 指定本地克隆目录 + public void testClone() throws Exception { + String repoURL = "http://example.com/your-repo.git"; + String username = "your-username"; + String password = "your-token-or-password"; String localPath = "./cloned-repo"; - log.info("克隆路径:" + new File(localPath).getAbsolutePath()); - // 如果目录已存在,则删除,确保每次都是全新克隆 + log.info("克隆路径:{}", new File(localPath).getAbsolutePath()); FileUtils.deleteDirectory(new File(localPath)); - // 使用 JGit 克隆仓库 Git git = Git.cloneRepository() .setURI(repoURL) .setDirectory(new File(localPath)) - .setCredentialsProvider( - new UsernamePasswordCredentialsProvider(username, password) - ) + .setCredentialsProvider(new UsernamePasswordCredentialsProvider(username, password)) .call(); - - // 关闭 Git 对象,释放资源 git.close(); } - /** - * 测试方法:遍历克隆下来的本地仓库文件,将每个文件读取为文档,拆分后存入向量库。 - * @throws IOException 在文件遍历或 IO 操作出错时抛出 - */ + /** 遍历本地仓库,将文件内容读取、拆分并写入向量库 */ @Test - public void test_file() throws IOException { + public void testFileUpload() throws Exception { Files.walkFileTree(Paths.get("./cloned-repo"), new SimpleFileVisitor<>() { @Override public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) { - // 跳过 .git 目录 - if (dir.getFileName().toString().equals(".git")) { + if (".git".equals(dir.getFileName().toString())) { return FileVisitResult.SKIP_SUBTREE; } return FileVisitResult.CONTINUE; } @Override - public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { - log.info("文件路径:{}", file); - - PathResource resource = new PathResource(file); - TikaDocumentReader reader = new TikaDocumentReader(resource); - - // 1) 先拿到不可变的列表 - List original = Collections.emptyList(); - try { - original = reader.get(); - } catch (Exception ex) { - log.warn("跳过无法读取的文件 {}: {}", file, ex.getMessage()); - return FileVisitResult.CONTINUE; - } - - // 2) 复制到可变列表 + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { + log.info("处理文件:{}", file); + TikaDocumentReader reader = new TikaDocumentReader(new PathResource(file)); + List original = reader.get(); List docs = new ArrayList<>(original); - - // 3) 去除 content 为空或纯空白的 Document - docs.removeIf(d -> d.getContent() == null || d.getContent().trim().isEmpty()); + docs.removeIf(d -> d.getText() == null || d.getText().trim().isEmpty()); if (docs.isEmpty()) { return FileVisitResult.CONTINUE; } - - // 4) 给文档打标签 docs.forEach(d -> d.getMetadata().put("knowledge", "group-buy-market")); - - // 5) 拆分成更小的段落 List splits = tokenTextSplitter.apply(docs); splits.forEach(d -> d.getMetadata().put("knowledge", "group-buy-market")); - - // 6) 写入向量存储 pgVectorStore.accept(splits); - return FileVisitResult.CONTINUE; } }); } - } diff --git a/ai-rag-knowledge-app/src/test/java/edu/whut/test/RAGTest.java b/ai-rag-knowledge-app/src/test/java/edu/whut/test/RAGTest.java index ddc6ffe..c5414bc 100644 --- a/ai-rag-knowledge-app/src/test/java/edu/whut/test/RAGTest.java +++ b/ai-rag-knowledge-app/src/test/java/edu/whut/test/RAGTest.java @@ -1,3 +1,4 @@ +// src/test/java/edu/whut/test/RAGTest.java package edu.whut.test; import com.alibaba.fastjson.JSON; @@ -5,21 +6,21 @@ import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.junit.Test; import org.junit.runner.RunWith; -import org.springframework.ai.chat.ChatResponse; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.ai.document.Document; +import org.springframework.ai.reader.tika.TikaDocumentReader; +import org.springframework.ai.transformer.splitter.TokenTextSplitter; +import org.springframework.ai.vectorstore.SimpleVectorStore; +import org.springframework.ai.vectorstore.pgvector.PgVectorStore; +import org.springframework.ai.vectorstore.SearchRequest; +import org.springframework.ai.ollama.OllamaChatModel; +import org.springframework.ai.ollama.api.OllamaOptions; +import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.messages.Message; import org.springframework.ai.chat.messages.UserMessage; import org.springframework.ai.chat.prompt.Prompt; import org.springframework.ai.chat.prompt.SystemPromptTemplate; -import org.springframework.ai.document.Document; -import org.springframework.ai.ollama.OllamaChatClient; -import org.springframework.ai.ollama.api.OllamaOptions; -import org.springframework.ai.reader.tika.TikaDocumentReader; -import org.springframework.ai.transformer.splitter.TokenTextSplitter; -import org.springframework.ai.vectorstore.PgVectorStore; -import org.springframework.ai.vectorstore.SearchRequest; -import org.springframework.ai.vectorstore.SimpleVectorStore; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.junit4.SpringRunner; import java.util.ArrayList; import java.util.List; @@ -27,95 +28,78 @@ import java.util.Map; import java.util.stream.Collectors; /** - * RAG 测试类,验证文档上传、向量存储和 Ollama 回答流程 + * RAG 测试类:验证文档上传、向量检索和 Ollama 回答流程 */ @Slf4j @RunWith(SpringRunner.class) @SpringBootTest public class RAGTest { - // 注入 Ollama 聊天客户端,用于直接测试模型调用 @Resource - private OllamaChatClient ollamaChatClient; + private OllamaChatModel ollamaChatModel; - // 注入 TokenTextSplitter,用于将长文档拆分为小段 @Resource private TokenTextSplitter tokenTextSplitter; - // 注入简单内存向量存储,用于快速测试 @Resource private SimpleVectorStore simpleVectorStore; - // 注入 PostgreSQL 向量存储,用于持久化测试 @Resource private PgVectorStore pgVectorStore; - /** - * 测试方法:读取本地文件,将其拆分并上传到 pgVectorStore - */ + /** 测试上传:将本地文件拆分并写入 pgvector */ @Test public void upload() { - // 使用 TikaDocumentReader 读取本地文件并提取文档对象 - TikaDocumentReader reader = new TikaDocumentReader("./data/file.text"); + TikaDocumentReader reader = new TikaDocumentReader("./data/file.txt"); List documents = reader.get(); - // 对提取的文档进行 Token 拆分 - List documentSplitterList = tokenTextSplitter.apply(documents); + List splits = tokenTextSplitter.apply(documents); - // 为原文档和拆分文档设置 "knowledge" 标签 - documents.forEach(doc -> doc.getMetadata().put("knowledge", "测试知识库名称")); - documentSplitterList.forEach(doc -> doc.getMetadata().put("knowledge", "测试知识库名称")); - - // 将拆分的文档批量存入 PostgreSQL pgvector 存储 - pgVectorStore.accept(documentSplitterList); + documents.forEach(d -> d.getMetadata().put("knowledge", "测试知识库名称")); + splits.forEach(d -> d.getMetadata().put("knowledge", "测试知识库名称")); + pgVectorStore.accept(splits); log.info("上传完成"); } - /** - * 测试方法:构造用户消息和系统提示,检索向量存储并调用 Ollama 获取回答 - */ + /** 测试检索+回答:查询向量库并调用 Ollama */ @Test public void chat() { - // 设置用户提问 - String message = "王大瓜,哪年出生"; + String message = "王大瓜,哪年出生?"; - // 定义系统提示模板,将检索到的文档注入,要求中文回答 String SYSTEM_PROMPT = """ - Use the information from the DOCUMENTS section to provide accurate answers but act as if you knew this information innately. - If unsure, simply state that you don't know. - Another thing you need to note is that your reply must be in Chinese! - DOCUMENTS: - {documents} - """; + Use the information from the DOCUMENTS section to provide accurate answers but act as if you knew this information innately. + If unsure, simply state that you don't know. + Another thing you need to note is that your reply must be in Chinese! + DOCUMENTS: + {documents} + """; - // 构建向量检索请求,最大返回 5 条,按 "knowledge" 标签过滤 - SearchRequest request = SearchRequest.query(message) - .withTopK(5) - .withFilterExpression("knowledge == '测试知识库名称'"); + // 构建检索请求 + SearchRequest request = SearchRequest.builder() + .query(message) + .topK(5) + .filterExpression("knowledge == '测试知识库名称'") + .build(); - // 执行相似度搜索,获取匹配的文档列表 - List documents = pgVectorStore.similaritySearch(request); - // 拼接所有文档内容 - String documentsCollectors = documents.stream() - .map(Document::getContent) + // 执行相似度搜索 + List docs = pgVectorStore.similaritySearch(request); + String docContent = docs.stream() + .map(Document::getText) .collect(Collectors.joining()); - // 创建带有文档内容的系统消息 + // 构建系统消息 Message ragMessage = new SystemPromptTemplate(SYSTEM_PROMPT) - .createMessage(Map.of("documents", documentsCollectors)); + .createMessage(Map.of("documents", docContent)); - // 构造消息列表:用户消息在前,系统消息在后 - ArrayList messages = new ArrayList<>(); + // 发起对话 + List messages = new ArrayList<>(); messages.add(new UserMessage(message)); messages.add(ragMessage); - // 调用 Ollama 同步接口获取模型回答 - ChatResponse chatResponse = ollamaChatClient.call( - new Prompt(messages, OllamaOptions.create().withModel("deepseek-r1:1.5b")) + ChatResponse response = ollamaChatModel.call( + new Prompt(messages, OllamaOptions.builder().model("deepseek-r1:1.5b").build()) ); - // 打印测试结果 - log.info("测试结果:{}", JSON.toJSONString(chatResponse)); + log.info("测试结果: {}", JSON.toJSONString(response)); } - } diff --git a/ai-rag-knowledge-trigger/src/main/java/edu/whut/trigger/http/OllamaController.java b/ai-rag-knowledge-trigger/src/main/java/edu/whut/trigger/http/OllamaController.java index 6323bf7..56b9cef 100644 --- a/ai-rag-knowledge-trigger/src/main/java/edu/whut/trigger/http/OllamaController.java +++ b/ai-rag-knowledge-trigger/src/main/java/edu/whut/trigger/http/OllamaController.java @@ -3,16 +3,16 @@ package edu.whut.trigger.http; import edu.whut.api.IAiService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.ai.chat.ChatResponse; import org.springframework.ai.chat.messages.Message; import org.springframework.ai.chat.messages.UserMessage; +import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.prompt.Prompt; import org.springframework.ai.chat.prompt.SystemPromptTemplate; import org.springframework.ai.document.Document; -import org.springframework.ai.ollama.OllamaChatClient; +import org.springframework.ai.ollama.OllamaChatModel; import org.springframework.ai.ollama.api.OllamaOptions; -import org.springframework.ai.vectorstore.PgVectorStore; import org.springframework.ai.vectorstore.SearchRequest; +import org.springframework.ai.vectorstore.pgvector.PgVectorStore; import org.springframework.web.bind.annotation.*; import reactor.core.publisher.Flux; @@ -31,47 +31,49 @@ import java.util.stream.Collectors; @Slf4j public class OllamaController implements IAiService { - // 注入 Ollama 对话客户端,用于向 Ollama 模型发起对话请求 - private final OllamaChatClient chatClient; + // 注入 Ollama 对话模型(替代旧版 OllamaChatClient) + private final OllamaChatModel ollamaChatModel; // 注入 PostgreSQL 向量存储,用于 RAG 检索 private final PgVectorStore pgVectorStore; /** * 普通生成接口,返回一次性 ChatResponse - * 示例: GET /generate?model=deepseek-r1:1.5b&message=1+1 + * 示例: GET /api/v1/ollama/generate?model=deepseek-r1:1.5b&message=1+1 */ @GetMapping("generate") @Override public ChatResponse generate( @RequestParam("model") String model, @RequestParam("message") String message) { - // 构建 Prompt 并调用 OllamaClient 同步获取响应 + log.info("generate called!"); - return chatClient.call( - new Prompt(message, OllamaOptions.create().withModel(model)) - ); + return ollamaChatModel.call(new Prompt( + message, + OllamaOptions.builder().model(model).build() + )); } /** - * 流式生成接口,返回 Flux<ChatResponse> - * 示例: GET /generate_stream?model=deepseek-r1:1.5b&message=hi + * 流式生成接口,返回 Flux + * 示例: GET /api/v1/ollama/generate_stream?model=deepseek-r1:1.5b&message=hi */ @GetMapping("generate_stream") @Override public Flux generateStream( @RequestParam("model") String model, @RequestParam("message") String message) { - // 调用 OllamaClient 的 stream 方法,开启 SSE 或分块传输 + log.info("generate_stream called!"); - return chatClient.stream( - new Prompt(message, OllamaOptions.create().withModel(model)) - ); + return ollamaChatModel.stream(new Prompt( + message, + OllamaOptions.builder().model(model).build() + )); } /** * RAG 流式生成接口:先检索相关文档,再附加系统提示,最后流式调用模型 - * 示例: GET /generate_stream_rag?model=deepseek-r1:1.5b&ragTag=xxx&message=内容 + * 示例: GET /api/v1/ollama/generate_stream_rag?model=deepseek-r1:1.5b&ragTag=xxx&message=内容 */ @GetMapping("generate_stream_rag") @Override @@ -79,10 +81,10 @@ public class OllamaController implements IAiService { @RequestParam("model") String model, @RequestParam("ragTag") String ragTag, @RequestParam("message") String message) { - log.info("generate_stream_rag called!"); - // 系统提示模板,嵌入检索到的文档内容,并要求中文回复 - String SYSTEM_PROMPT = - """ + + log.info("generate_stream_rag called!用户问题是:"+message); + + String SYSTEM_PROMPT = """ Use the information from the DOCUMENTS section to provide accurate answers but act as if you knew this information innately. If unsure, simply state that you don't know. Another thing you need to note is that your reply must be in Chinese! @@ -90,31 +92,29 @@ public class OllamaController implements IAiService { {documents} """; - // 构建检索请求:基于用户 message 检索 TopK 文档,并使用 ragTag 过滤标签 - SearchRequest request = SearchRequest.query(message) - .withTopK(5) - .withFilterExpression("knowledge == '" + ragTag + "'"); + // 基于用户 message 检索 TopK 文档,并使用 ragTag 过滤标签 + SearchRequest request = SearchRequest.builder() + .query(message) + .topK(5) + .filterExpression("knowledge == '" + ragTag + "'") + .build(); - // 执行相似度搜索,获取匹配文档列表 List documents = pgVectorStore.similaritySearch(request); - // 拼接文档内容 + // M6 文档对象推荐使用 getText() String documentContent = documents.stream() - .map(Document::getContent) + .map(Document::getText) .collect(Collectors.joining()); - // 使用 SystemPromptTemplate 注入文档到系统消息 Message ragMessage = new SystemPromptTemplate(SYSTEM_PROMPT) .createMessage(Map.of("documents", documentContent)); - // 构造消息列表:先用户消息,再系统消息 List messages = new ArrayList<>(); messages.add(new UserMessage(message)); messages.add(ragMessage); - // 发起流式调用 - return chatClient.stream( - new Prompt(messages, OllamaOptions.create().withModel(model)) - ); + return ollamaChatModel.stream(new Prompt( + messages, + OllamaOptions.builder().model(model).build() + )); } - } diff --git a/ai-rag-knowledge-trigger/src/main/java/edu/whut/trigger/http/RAGController.java b/ai-rag-knowledge-trigger/src/main/java/edu/whut/trigger/http/RAGController.java index fa0ba51..d44f754 100644 --- a/ai-rag-knowledge-trigger/src/main/java/edu/whut/trigger/http/RAGController.java +++ b/ai-rag-knowledge-trigger/src/main/java/edu/whut/trigger/http/RAGController.java @@ -2,7 +2,6 @@ package edu.whut.trigger.http; import edu.whut.api.IRAGService; import edu.whut.api.response.Response; -import jakarta.annotation.Resource; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.io.FileUtils; @@ -11,11 +10,11 @@ import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider; import org.redisson.api.RList; import org.redisson.api.RedissonClient; import org.springframework.ai.document.Document; -import org.springframework.ai.ollama.OllamaChatClient; +import org.springframework.ai.ollama.OllamaChatModel; import org.springframework.ai.reader.tika.TikaDocumentReader; import org.springframework.ai.transformer.splitter.TokenTextSplitter; -import org.springframework.ai.vectorstore.PgVectorStore; import org.springframework.ai.vectorstore.SimpleVectorStore; +import org.springframework.ai.vectorstore.pgvector.PgVectorStore; import org.springframework.core.io.PathResource; import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.*; @@ -38,8 +37,8 @@ import java.util.List; @RequiredArgsConstructor public class RAGController implements IRAGService { - // Ollama 聊天客户端,用于后续可能的对话调用(此处暂无直接使用) - private final OllamaChatClient ollamaChatClient; + // Ollama 对话模型(此处暂无直接使用,但保留注入以便扩展) + private final OllamaChatModel ollamaChatModel; // 文本拆分器,将长文档切分为合适大小的段落或 Token 块 private final TokenTextSplitter tokenTextSplitter; @@ -62,6 +61,20 @@ public class RAGController implements IRAGService { public Response> queryRagTagList() { // 从 Redis 列表获取所有标签 RList elements = redissonClient.getList("ragTag"); + // 读一个快照,便于安全日志与返回 + List tags; + try { + tags = elements.readAll(); // Redisson 提供的批量读取 + } catch (Exception e) { + // 兜底:某些客户端/版本没有 readAll 时使用迭代 + log.warn("读取 Redis ragTag 列表使用 readAll 失败,改用迭代读取。", e); + tags = new ArrayList<>(); + for (String s : elements) { + tags.add(s); + } + } + // 打印查询到的标签(数量 + 内容) + log.info("查询 RAG 标签列表,数量:{},内容:{}", tags.size(), tags); return Response.>builder() .code("0000") .info("调用成功") @@ -81,7 +94,7 @@ public class RAGController implements IRAGService { public Response uploadFile( @RequestParam("ragTag") String ragTag, @RequestParam("file") List files, - @RequestParam(value = "filePath", required = false) List filePaths ) { + @RequestParam(value = "filePath", required = false) List filePaths) { log.info("上传知识库开始:{}", ragTag); @@ -98,41 +111,41 @@ public class RAGController implements IRAGService { // 可选:调试日志 log.debug("接收文件:{},相对路径:{}", file.getOriginalFilename(), relPath); - // 读取上传文件,提取文档内容 - TikaDocumentReader documentReader = new TikaDocumentReader(file.getResource()); - List documents = documentReader.get(); + try { + // 读取上传文件,提取文档内容 + TikaDocumentReader documentReader = new TikaDocumentReader(file.getResource()); + List documents = documentReader.get(); - // 对文档进行 Token 拆分 - List documentSplitterList = tokenTextSplitter.apply(documents); + // 对文档进行 Token 拆分 + List documentSplitterList = tokenTextSplitter.apply(documents); - // 为原文档和拆分文档设置 ragTag 元数据 - documents.forEach(doc -> { - doc.getMetadata().put("knowledge", ragTag); - // ===== 新增:写入相对路径 ===== - doc.getMetadata().put("path", relPath); - }); - documentSplitterList.forEach(doc -> { - doc.getMetadata().put("knowledge", ragTag); - // ===== 新增:写入相对路径 ===== - doc.getMetadata().put("path", relPath); - }); + // 为原文档和拆分文档设置 ragTag 元数据 + 相对路径 + documents.forEach(doc -> { + doc.getMetadata().put("knowledge", ragTag); + doc.getMetadata().put("path", relPath); + }); + documentSplitterList.forEach(doc -> { + doc.getMetadata().put("knowledge", ragTag); + doc.getMetadata().put("path", relPath); + }); - // 存储拆分后的文档到 pgVectorStore - pgVectorStore.accept(documentSplitterList); - - // 更新 Redis 标签列表,避免重复 - RList elements = redissonClient.getList("ragTag"); - if (!elements.contains(ragTag)) { - elements.add(ragTag); + // 存储拆分后的文档到 pgVectorStore + pgVectorStore.accept(documentSplitterList); + } catch (Exception e) { + log.error("文件处理失败:{} - {}", file.getOriginalFilename(), e.getMessage(), e); } } + // 更新 Redis 标签列表,避免重复 + RList elements = redissonClient.getList("ragTag"); + if (!elements.contains(ragTag)) { + elements.add(ragTag); + } + log.info("上传知识库完成:{}", ragTag); return Response.builder().code("0000").info("调用成功").build(); } - - /** * 克隆并分析 Git 仓库: * - 克隆指定仓库到本地 @@ -179,7 +192,7 @@ public class RAGController implements IRAGService { // 2.2 复制为可变列表并过滤掉空内容 List docs = new ArrayList<>(raw); - docs.removeIf(d -> d.getContent() == null || d.getContent().trim().isEmpty()); + docs.removeIf(d -> d.getText() == null || d.getText().trim().isEmpty()); if (docs.isEmpty()) { return FileVisitResult.CONTINUE; } @@ -222,6 +235,42 @@ public class RAGController implements IRAGService { return Response.builder().code("0000").info("调用成功").build(); } + /** + * 测试接口 + * @return + */ + @GetMapping("knowledge/_ping") + public Response ragPing() { + log.info("RAG knowledge ping"); + return Response.builder().code("0000").info("ok").build(); + } + + + /** + * 删除指定知识库(按 ragTag): + * - 从 pgvector 向量库中删除所有 metadata.knowledge == ragTag 的文档 + * - 从 Redis ragTag 列表中删除该标签 + * DELETE /api/v1/rag/knowledge/{ragTag} + */ + @DeleteMapping("knowledge/{ragTag}") + public Response deleteKnowledge(@PathVariable("ragTag") String ragTag) { + log.info("删除知识库开始:{}", ragTag); + try { + // 1) 删除向量库中对应知识库的所有向量(1.0.0-M6:支持过滤表达式删除) + pgVectorStore.delete("knowledge == '" + ragTag + "'"); + + // 2) 从 Redis 标签列表移除 + RList elements = redissonClient.getList("ragTag"); + elements.remove(ragTag); + + log.info("删除知识库完成:{}", ragTag); + return Response.builder().code("0000").info("删除成功").build(); + } catch (Exception e) { + log.error("删除知识库失败:{}", ragTag, e); + return Response.builder().code("9999").info("删除失败:" + e.getMessage()).build(); + } + } + /** * 从 Git 仓库 URL 提取项目名称(去除 .git 后缀) */ diff --git a/docs/rag-dev-ops/nginx/html/git.html b/docs/rag-dev-ops/nginx/html/git.html index 903e1e0..bbbdfc1 100644 --- a/docs/rag-dev-ops/nginx/html/git.html +++ b/docs/rag-dev-ops/nginx/html/git.html @@ -115,7 +115,7 @@ loadingOverlay.style.display = 'flex'; document.getElementById('status').textContent = ''; - fetch('http://localhost:8090/api/v1/rag/analyze_git_repository', { + fetch('http://localhost:8095/api/v1/rag/analyze_git_repository', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', diff --git a/docs/rag-dev-ops/nginx/html/upload.html b/docs/rag-dev-ops/nginx/html/upload.html index bfec6f1..f8e709c 100644 --- a/docs/rag-dev-ops/nginx/html/upload.html +++ b/docs/rag-dev-ops/nginx/html/upload.html @@ -128,7 +128,7 @@ formData.append('ragTag', document.getElementById('title').value); files.forEach(file => formData.append('file', file)); - axios.post('http://localhost:8090/api/v1/rag/file/upload', formData) + axios.post('http://localhost:8095/api/v1/rag/file/upload', formData) .then(response => { if (response.data.code === '0000') { // 成功提示并关闭窗口 diff --git a/docs/tag/v1.0/api/curl.sh b/docs/tag/v1.0/api/curl.sh index 8f4636c..0cf2862 100644 --- a/docs/tag/v1.0/api/curl.sh +++ b/docs/tag/v1.0/api/curl.sh @@ -1,9 +1 @@ -# A. 测试 embeddings(nomic-embed-text) -curl -s http://localhost:11434/api/embeddings \ - -H "Content-Type: application/json" \ - -d '{"model":"nomic-embed-text","input":"hello"}' - -# B. 测试 generate(deepseek-r1:1.5b) -curl -s http://localhost:11434/api/generate \ - -H "Content-Type: application/json" \ - -d '{"model":"deepseek-r1:1.5b","prompt":"say hi"}' +curl http://localhost:8095/api/v1/rag/query_rag_tag_list \ No newline at end of file diff --git a/docs/tag/v1.0/log/log-info-2025-07-28.0.log b/docs/tag/v1.0/log/log-info-2025-07-28.0.log deleted file mode 100644 index 60fab45..0000000 --- a/docs/tag/v1.0/log/log-info-2025-07-28.0.log +++ /dev/null @@ -1,20 +0,0 @@ -25-07-28.21:24:55.145 [main ] INFO Application - Starting Application v1.0 using Java 17.0.2 with PID 1 (/app.jar started by root in /) -25-07-28.21:24:55.150 [main ] INFO Application - The following 1 profile is active: "dev" -25-07-28.21:24:56.064 [main ] INFO RepositoryConfigurationDelegate - Multiple Spring Data modules found, entering strict repository configuration mode -25-07-28.21:24:56.067 [main ] INFO RepositoryConfigurationDelegate - Bootstrapping Spring Data Redis repositories in DEFAULT mode. -25-07-28.21:24:56.097 [main ] INFO RepositoryConfigurationDelegate - Finished Spring Data repository scanning in 14 ms. Found 0 Redis repository interfaces. -25-07-28.21:24:56.879 [main ] INFO TomcatWebServer - Tomcat initialized with port 8095 (http) -25-07-28.21:24:56.891 [main ] INFO Http11NioProtocol - Initializing ProtocolHandler ["http-nio-8095"] -25-07-28.21:24:56.893 [main ] INFO StandardService - Starting service [Tomcat] -25-07-28.21:24:56.894 [main ] INFO StandardEngine - Starting Servlet engine: [Apache Tomcat/10.1.19] -25-07-28.21:24:56.936 [main ] INFO [/] - Initializing Spring embedded WebApplicationContext -25-07-28.21:24:56.936 [main ] INFO ServletWebServerApplicationContext - Root WebApplicationContext: initialization completed in 1688 ms -25-07-28.21:24:57.400 [main ] INFO HikariDataSource - HikariCP - Starting... -25-07-28.21:24:57.627 [main ] INFO HikariPool - HikariCP - Added connection org.postgresql.jdbc.PgConnection@1c2dd89b -25-07-28.21:24:57.629 [main ] INFO HikariDataSource - HikariCP - Start completed. -25-07-28.21:25:01.124 [main ] INFO Version - Redisson 3.44.0 -25-07-28.21:25:01.444 [redisson-netty-1-5] INFO ConnectionsHolder - 1 connections initialized for 192.168.10.218/192.168.10.218:26379 -25-07-28.21:25:01.491 [redisson-netty-1-13] INFO ConnectionsHolder - 5 connections initialized for 192.168.10.218/192.168.10.218:26379 -25-07-28.21:25:02.123 [main ] INFO Http11NioProtocol - Starting ProtocolHandler ["http-nio-8095"] -25-07-28.21:25:02.135 [main ] INFO TomcatWebServer - Tomcat started on port 8095 (http) with context path '' -25-07-28.21:25:02.146 [main ] INFO Application - Started Application in 7.872 seconds (process running for 8.608) diff --git a/docs/tag/v1.0/log/log_error.log b/docs/tag/v1.0/log/log_error.log deleted file mode 100644 index e69de29..0000000 diff --git a/docs/tag/v1.0/log/log_info.log b/docs/tag/v1.0/log/log_info.log deleted file mode 100644 index 4cae312..0000000 --- a/docs/tag/v1.0/log/log_info.log +++ /dev/null @@ -1,229 +0,0 @@ -25-07-29.08:46:26.334 [main ] INFO Application - Starting Application v1.0 using Java 17.0.2 with PID 1 (/app.jar started by root in /) -25-07-29.08:46:26.343 [main ] INFO Application - The following 1 profile is active: "dev" -25-07-29.08:46:27.850 [main ] INFO RepositoryConfigurationDelegate - Multiple Spring Data modules found, entering strict repository configuration mode -25-07-29.08:46:27.857 [main ] INFO RepositoryConfigurationDelegate - Bootstrapping Spring Data Redis repositories in DEFAULT mode. -25-07-29.08:46:27.927 [main ] INFO RepositoryConfigurationDelegate - Finished Spring Data repository scanning in 27 ms. Found 0 Redis repository interfaces. -25-07-29.08:46:29.637 [main ] INFO TomcatWebServer - Tomcat initialized with port 8095 (http) -25-07-29.08:46:29.656 [main ] INFO Http11NioProtocol - Initializing ProtocolHandler ["http-nio-8095"] -25-07-29.08:46:29.661 [main ] INFO StandardService - Starting service [Tomcat] -25-07-29.08:46:29.661 [main ] INFO StandardEngine - Starting Servlet engine: [Apache Tomcat/10.1.19] -25-07-29.08:46:29.739 [main ] INFO [/] - Initializing Spring embedded WebApplicationContext -25-07-29.08:46:29.741 [main ] INFO ServletWebServerApplicationContext - Root WebApplicationContext: initialization completed in 3239 ms -25-07-29.08:46:30.753 [main ] INFO HikariDataSource - HikariCP - Starting... -25-07-29.08:46:31.046 [main ] INFO HikariPool - HikariCP - Added connection org.postgresql.jdbc.PgConnection@7dd45c93 -25-07-29.08:46:31.049 [main ] INFO HikariDataSource - HikariCP - Start completed. -25-07-29.08:46:36.257 [main ] INFO Version - Redisson 3.44.0 -25-07-29.08:46:36.850 [redisson-netty-1-4] INFO ConnectionsHolder - 1 connections initialized for 192.168.10.218/192.168.10.218:26379 -25-07-29.08:46:36.921 [redisson-netty-1-13] INFO ConnectionsHolder - 5 connections initialized for 192.168.10.218/192.168.10.218:26379 -25-07-29.08:46:38.094 [main ] INFO Http11NioProtocol - Starting ProtocolHandler ["http-nio-8095"] -25-07-29.08:46:38.110 [main ] INFO TomcatWebServer - Tomcat started on port 8095 (http) with context path '' -25-07-29.08:46:38.124 [main ] INFO Application - Started Application in 13.451 seconds (process running for 14.471) -25-07-29.08:51:25.055 [SpringApplicationShutdownHook] INFO HikariDataSource - HikariCP - Shutdown initiated... -25-07-29.08:51:25.059 [SpringApplicationShutdownHook] INFO HikariDataSource - HikariCP - Shutdown completed. -25-07-29.08:51:40.866 [main ] INFO Application - Starting Application v1.0 using Java 17.0.2 with PID 1 (/app.jar started by root in /) -25-07-29.08:51:40.876 [main ] INFO Application - The following 1 profile is active: "dev" -25-07-29.08:51:41.638 [main ] INFO RepositoryConfigurationDelegate - Multiple Spring Data modules found, entering strict repository configuration mode -25-07-29.08:51:41.641 [main ] INFO RepositoryConfigurationDelegate - Bootstrapping Spring Data Redis repositories in DEFAULT mode. -25-07-29.08:51:41.665 [main ] INFO RepositoryConfigurationDelegate - Finished Spring Data repository scanning in 11 ms. Found 0 Redis repository interfaces. -25-07-29.08:51:42.318 [main ] INFO TomcatWebServer - Tomcat initialized with port 8095 (http) -25-07-29.08:51:42.326 [main ] INFO Http11NioProtocol - Initializing ProtocolHandler ["http-nio-8095"] -25-07-29.08:51:42.328 [main ] INFO StandardService - Starting service [Tomcat] -25-07-29.08:51:42.329 [main ] INFO StandardEngine - Starting Servlet engine: [Apache Tomcat/10.1.19] -25-07-29.08:51:42.369 [main ] INFO [/] - Initializing Spring embedded WebApplicationContext -25-07-29.08:51:42.370 [main ] INFO ServletWebServerApplicationContext - Root WebApplicationContext: initialization completed in 1395 ms -25-07-29.08:51:42.702 [main ] INFO HikariDataSource - HikariCP - Starting... -25-07-29.08:51:42.868 [main ] INFO HikariPool - HikariCP - Added connection org.postgresql.jdbc.PgConnection@7e3ee128 -25-07-29.08:51:42.869 [main ] INFO HikariDataSource - HikariCP - Start completed. -25-07-29.08:51:46.030 [main ] INFO Version - Redisson 3.44.0 -25-07-29.08:51:46.329 [redisson-netty-1-5] INFO ConnectionsHolder - 1 connections initialized for 192.168.10.218/192.168.10.218:26379 -25-07-29.08:51:46.371 [redisson-netty-1-13] INFO ConnectionsHolder - 5 connections initialized for 192.168.10.218/192.168.10.218:26379 -25-07-29.08:51:47.114 [main ] INFO Http11NioProtocol - Starting ProtocolHandler ["http-nio-8095"] -25-07-29.08:51:47.132 [main ] INFO TomcatWebServer - Tomcat started on port 8095 (http) with context path '' -25-07-29.08:51:47.148 [main ] INFO Application - Started Application in 7.034 seconds (process running for 7.668) -25-07-29.09:04:24.859 [SpringApplicationShutdownHook] INFO HikariDataSource - HikariCP - Shutdown initiated... -25-07-29.09:04:24.865 [SpringApplicationShutdownHook] INFO HikariDataSource - HikariCP - Shutdown completed. -25-07-29.09:04:35.408 [main ] INFO Application - Starting Application v1.0 using Java 17.0.2 with PID 1 (/app.jar started by root in /) -25-07-29.09:04:35.412 [main ] INFO Application - The following 1 profile is active: "dev" -25-07-29.09:04:36.258 [main ] INFO RepositoryConfigurationDelegate - Multiple Spring Data modules found, entering strict repository configuration mode -25-07-29.09:04:36.261 [main ] INFO RepositoryConfigurationDelegate - Bootstrapping Spring Data Redis repositories in DEFAULT mode. -25-07-29.09:04:36.291 [main ] INFO RepositoryConfigurationDelegate - Finished Spring Data repository scanning in 13 ms. Found 0 Redis repository interfaces. -25-07-29.09:04:36.989 [main ] INFO TomcatWebServer - Tomcat initialized with port 8095 (http) -25-07-29.09:04:36.999 [main ] INFO Http11NioProtocol - Initializing ProtocolHandler ["http-nio-8095"] -25-07-29.09:04:37.001 [main ] INFO StandardService - Starting service [Tomcat] -25-07-29.09:04:37.001 [main ] INFO StandardEngine - Starting Servlet engine: [Apache Tomcat/10.1.19] -25-07-29.09:04:37.043 [main ] INFO [/] - Initializing Spring embedded WebApplicationContext -25-07-29.09:04:37.044 [main ] INFO ServletWebServerApplicationContext - Root WebApplicationContext: initialization completed in 1536 ms -25-07-29.09:04:37.484 [main ] INFO HikariDataSource - HikariCP - Starting... -25-07-29.09:04:37.701 [main ] INFO HikariPool - HikariCP - Added connection org.postgresql.jdbc.PgConnection@7e3ee128 -25-07-29.09:04:37.702 [main ] INFO HikariDataSource - HikariCP - Start completed. -25-07-29.09:04:40.658 [main ] INFO Version - Redisson 3.44.0 -25-07-29.09:04:40.910 [redisson-netty-1-4] INFO ConnectionsHolder - 1 connections initialized for 192.168.10.218/192.168.10.218:26379 -25-07-29.09:04:40.948 [redisson-netty-1-13] INFO ConnectionsHolder - 5 connections initialized for 192.168.10.218/192.168.10.218:26379 -25-07-29.09:04:41.615 [main ] INFO Http11NioProtocol - Starting ProtocolHandler ["http-nio-8095"] -25-07-29.09:04:41.624 [main ] INFO TomcatWebServer - Tomcat started on port 8095 (http) with context path '' -25-07-29.09:04:41.632 [main ] INFO Application - Started Application in 7.051 seconds (process running for 7.813) -25-07-29.09:26:52.922 [SpringApplicationShutdownHook] INFO HikariDataSource - HikariCP - Shutdown initiated... -25-07-29.09:26:52.925 [SpringApplicationShutdownHook] INFO HikariDataSource - HikariCP - Shutdown completed. -25-07-29.09:28:58.726 [main ] INFO Application - Starting Application v1.0 using Java 17.0.2 with PID 1 (/app.jar started by root in /) -25-07-29.09:28:58.730 [main ] INFO Application - The following 1 profile is active: "dev" -25-07-29.09:28:59.560 [main ] INFO RepositoryConfigurationDelegate - Multiple Spring Data modules found, entering strict repository configuration mode -25-07-29.09:28:59.563 [main ] INFO RepositoryConfigurationDelegate - Bootstrapping Spring Data Redis repositories in DEFAULT mode. -25-07-29.09:28:59.595 [main ] INFO RepositoryConfigurationDelegate - Finished Spring Data repository scanning in 14 ms. Found 0 Redis repository interfaces. -25-07-29.09:29:00.294 [main ] INFO TomcatWebServer - Tomcat initialized with port 8095 (http) -25-07-29.09:29:00.306 [main ] INFO Http11NioProtocol - Initializing ProtocolHandler ["http-nio-8095"] -25-07-29.09:29:00.308 [main ] INFO StandardService - Starting service [Tomcat] -25-07-29.09:29:00.308 [main ] INFO StandardEngine - Starting Servlet engine: [Apache Tomcat/10.1.19] -25-07-29.09:29:00.349 [main ] INFO [/] - Initializing Spring embedded WebApplicationContext -25-07-29.09:29:00.349 [main ] INFO ServletWebServerApplicationContext - Root WebApplicationContext: initialization completed in 1535 ms -25-07-29.09:29:00.755 [main ] INFO HikariDataSource - HikariCP - Starting... -25-07-29.09:29:00.938 [main ] INFO HikariPool - HikariCP - Added connection org.postgresql.jdbc.PgConnection@8383a14 -25-07-29.09:29:00.940 [main ] INFO HikariDataSource - HikariCP - Start completed. -25-07-29.09:29:04.144 [main ] INFO Version - Redisson 3.44.0 -25-07-29.09:29:04.413 [redisson-netty-1-5] INFO ConnectionsHolder - 1 connections initialized for 192.168.10.218/192.168.10.218:26379 -25-07-29.09:29:04.451 [redisson-netty-1-13] INFO ConnectionsHolder - 5 connections initialized for 192.168.10.218/192.168.10.218:26379 -25-07-29.09:29:05.056 [main ] INFO Http11NioProtocol - Starting ProtocolHandler ["http-nio-8095"] -25-07-29.09:29:05.069 [main ] INFO TomcatWebServer - Tomcat started on port 8095 (http) with context path '' -25-07-29.09:29:05.078 [main ] INFO Application - Started Application in 7.161 seconds (process running for 7.867) -25-07-29.09:29:58.580 [SpringApplicationShutdownHook] INFO HikariDataSource - HikariCP - Shutdown initiated... -25-07-29.09:29:58.582 [SpringApplicationShutdownHook] INFO HikariDataSource - HikariCP - Shutdown completed. -25-07-29.09:30:20.134 [main ] INFO Application - Starting Application v1.0 using Java 17.0.2 with PID 1 (/app.jar started by root in /) -25-07-29.09:30:20.139 [main ] INFO Application - The following 1 profile is active: "dev" -25-07-29.09:30:20.995 [main ] INFO RepositoryConfigurationDelegate - Multiple Spring Data modules found, entering strict repository configuration mode -25-07-29.09:30:20.998 [main ] INFO RepositoryConfigurationDelegate - Bootstrapping Spring Data Redis repositories in DEFAULT mode. -25-07-29.09:30:21.032 [main ] INFO RepositoryConfigurationDelegate - Finished Spring Data repository scanning in 14 ms. Found 0 Redis repository interfaces. -25-07-29.09:30:21.718 [main ] INFO TomcatWebServer - Tomcat initialized with port 8095 (http) -25-07-29.09:30:21.728 [main ] INFO Http11NioProtocol - Initializing ProtocolHandler ["http-nio-8095"] -25-07-29.09:30:21.731 [main ] INFO StandardService - Starting service [Tomcat] -25-07-29.09:30:21.732 [main ] INFO StandardEngine - Starting Servlet engine: [Apache Tomcat/10.1.19] -25-07-29.09:30:21.771 [main ] INFO [/] - Initializing Spring embedded WebApplicationContext -25-07-29.09:30:21.772 [main ] INFO ServletWebServerApplicationContext - Root WebApplicationContext: initialization completed in 1534 ms -25-07-29.09:30:22.137 [main ] INFO HikariDataSource - HikariCP - Starting... -25-07-29.09:30:22.308 [main ] INFO HikariPool - HikariCP - Added connection org.postgresql.jdbc.PgConnection@374b6e33 -25-07-29.09:30:22.310 [main ] INFO HikariDataSource - HikariCP - Start completed. -25-07-29.09:30:22.970 [main ] INFO Version - Redisson 3.44.0 -25-07-29.09:30:23.231 [redisson-netty-1-5] INFO ConnectionsHolder - 1 connections initialized for 192.168.10.218/192.168.10.218:26379 -25-07-29.09:30:23.271 [redisson-netty-1-13] INFO ConnectionsHolder - 5 connections initialized for 192.168.10.218/192.168.10.218:26379 -25-07-29.09:30:24.044 [main ] INFO Http11NioProtocol - Starting ProtocolHandler ["http-nio-8095"] -25-07-29.09:30:24.053 [main ] INFO TomcatWebServer - Tomcat started on port 8095 (http) with context path '' -25-07-29.09:30:24.063 [main ] INFO Application - Started Application in 4.689 seconds (process running for 5.236) -25-07-29.09:30:24.980 [http-nio-8095-exec-1] INFO [/] - Initializing Spring DispatcherServlet 'dispatcherServlet' -25-07-29.09:30:24.981 [http-nio-8095-exec-1] INFO DispatcherServlet - Initializing Servlet 'dispatcherServlet' -25-07-29.09:30:24.982 [http-nio-8095-exec-1] INFO DispatcherServlet - Completed initialization in 1 ms -25-07-29.09:31:54.384 [http-nio-8095-exec-2] INFO OllamaController - generate_stream called! -25-07-29.09:44:47.629 [SpringApplicationShutdownHook] INFO HikariDataSource - HikariCP - Shutdown initiated... -25-07-29.09:44:47.633 [SpringApplicationShutdownHook] INFO HikariDataSource - HikariCP - Shutdown completed. -25-07-29.09:45:42.997 [main ] INFO Application - Starting Application v1.0 using Java 17.0.2 with PID 1 (/app.jar started by root in /) -25-07-29.09:45:43.002 [main ] INFO Application - The following 1 profile is active: "dev" -25-07-29.09:45:43.847 [main ] INFO RepositoryConfigurationDelegate - Multiple Spring Data modules found, entering strict repository configuration mode -25-07-29.09:45:43.850 [main ] INFO RepositoryConfigurationDelegate - Bootstrapping Spring Data Redis repositories in DEFAULT mode. -25-07-29.09:45:43.880 [main ] INFO RepositoryConfigurationDelegate - Finished Spring Data repository scanning in 12 ms. Found 0 Redis repository interfaces. -25-07-29.09:45:44.636 [main ] INFO TomcatWebServer - Tomcat initialized with port 8095 (http) -25-07-29.09:45:44.648 [main ] INFO Http11NioProtocol - Initializing ProtocolHandler ["http-nio-8095"] -25-07-29.09:45:44.649 [main ] INFO StandardService - Starting service [Tomcat] -25-07-29.09:45:44.649 [main ] INFO StandardEngine - Starting Servlet engine: [Apache Tomcat/10.1.19] -25-07-29.09:45:44.693 [main ] INFO [/] - Initializing Spring embedded WebApplicationContext -25-07-29.09:45:44.693 [main ] INFO ServletWebServerApplicationContext - Root WebApplicationContext: initialization completed in 1578 ms -25-07-29.09:45:45.142 [main ] INFO HikariDataSource - HikariCP - Starting... -25-07-29.09:45:45.363 [main ] INFO HikariPool - HikariCP - Added connection org.postgresql.jdbc.PgConnection@6ba060af -25-07-29.09:45:45.365 [main ] INFO HikariDataSource - HikariCP - Start completed. -25-07-29.09:45:48.289 [main ] INFO Version - Redisson 3.44.0 -25-07-29.09:45:48.586 [redisson-netty-1-4] INFO ConnectionsHolder - 1 connections initialized for 192.168.10.218/192.168.10.218:26379 -25-07-29.09:45:48.623 [redisson-netty-1-13] INFO ConnectionsHolder - 5 connections initialized for 192.168.10.218/192.168.10.218:26379 -25-07-29.09:45:49.297 [main ] INFO Http11NioProtocol - Starting ProtocolHandler ["http-nio-8095"] -25-07-29.09:45:49.310 [main ] INFO TomcatWebServer - Tomcat started on port 8095 (http) with context path '' -25-07-29.09:45:49.321 [main ] INFO Application - Started Application in 7.098 seconds (process running for 7.853) -25-07-29.09:45:51.712 [http-nio-8095-exec-1] INFO [/] - Initializing Spring DispatcherServlet 'dispatcherServlet' -25-07-29.09:45:51.713 [http-nio-8095-exec-1] INFO DispatcherServlet - Initializing Servlet 'dispatcherServlet' -25-07-29.09:45:51.713 [http-nio-8095-exec-1] INFO DispatcherServlet - Completed initialization in 0 ms -25-07-29.09:51:07.030 [SpringApplicationShutdownHook] INFO HikariDataSource - HikariCP - Shutdown initiated... -25-07-29.09:51:07.032 [SpringApplicationShutdownHook] INFO HikariDataSource - HikariCP - Shutdown completed. -25-07-29.09:51:16.935 [main ] INFO Application - Starting Application v1.0 using Java 17.0.2 with PID 1 (/app.jar started by root in /) -25-07-29.09:51:16.940 [main ] INFO Application - The following 1 profile is active: "dev" -25-07-29.09:51:17.677 [main ] INFO RepositoryConfigurationDelegate - Multiple Spring Data modules found, entering strict repository configuration mode -25-07-29.09:51:17.680 [main ] INFO RepositoryConfigurationDelegate - Bootstrapping Spring Data Redis repositories in DEFAULT mode. -25-07-29.09:51:17.708 [main ] INFO RepositoryConfigurationDelegate - Finished Spring Data repository scanning in 12 ms. Found 0 Redis repository interfaces. -25-07-29.09:51:18.510 [main ] INFO TomcatWebServer - Tomcat initialized with port 8095 (http) -25-07-29.09:51:18.521 [main ] INFO Http11NioProtocol - Initializing ProtocolHandler ["http-nio-8095"] -25-07-29.09:51:18.524 [main ] INFO StandardService - Starting service [Tomcat] -25-07-29.09:51:18.524 [main ] INFO StandardEngine - Starting Servlet engine: [Apache Tomcat/10.1.19] -25-07-29.09:51:18.572 [main ] INFO [/] - Initializing Spring embedded WebApplicationContext -25-07-29.09:51:18.572 [main ] INFO ServletWebServerApplicationContext - Root WebApplicationContext: initialization completed in 1540 ms -25-07-29.09:51:18.994 [main ] INFO HikariDataSource - HikariCP - Starting... -25-07-29.09:51:19.185 [main ] INFO HikariPool - HikariCP - Added connection org.postgresql.jdbc.PgConnection@192b472d -25-07-29.09:51:19.187 [main ] INFO HikariDataSource - HikariCP - Start completed. -25-07-29.09:51:22.365 [main ] INFO Version - Redisson 3.44.0 -25-07-29.09:51:22.643 [redisson-netty-1-4] INFO ConnectionsHolder - 1 connections initialized for 192.168.10.218/192.168.10.218:26379 -25-07-29.09:51:22.681 [redisson-netty-1-13] INFO ConnectionsHolder - 5 connections initialized for 192.168.10.218/192.168.10.218:26379 -25-07-29.09:51:23.441 [main ] INFO Http11NioProtocol - Starting ProtocolHandler ["http-nio-8095"] -25-07-29.09:51:23.451 [main ] INFO TomcatWebServer - Tomcat started on port 8095 (http) with context path '' -25-07-29.09:51:23.461 [main ] INFO Application - Started Application in 7.317 seconds (process running for 7.862) -25-07-29.09:51:35.181 [http-nio-8095-exec-1] INFO [/] - Initializing Spring DispatcherServlet 'dispatcherServlet' -25-07-29.09:51:35.181 [http-nio-8095-exec-1] INFO DispatcherServlet - Initializing Servlet 'dispatcherServlet' -25-07-29.09:51:35.182 [http-nio-8095-exec-1] INFO DispatcherServlet - Completed initialization in 0 ms -25-07-29.09:51:35.206 [http-nio-8095-exec-1] INFO OllamaController - generate_stream called! -25-07-29.09:53:05.853 [http-nio-8095-exec-5] INFO RAGController - 上传知识库开始:草稿1 -25-07-29.09:53:06.647 [http-nio-8095-exec-5] INFO RAGController - 上传知识库完成:草稿1 -25-07-29.09:53:30.890 [http-nio-8095-exec-7] INFO OllamaController - generate_stream_rag called! -25-07-29.10:26:15.821 [SpringApplicationShutdownHook] INFO HikariDataSource - HikariCP - Shutdown initiated... -25-07-29.10:26:15.826 [SpringApplicationShutdownHook] INFO HikariDataSource - HikariCP - Shutdown completed. -25-07-29.10:27:07.258 [main ] INFO Application - Starting Application v1.0 using Java 17.0.2 with PID 1 (/app.jar started by root in /) -25-07-29.10:27:07.263 [main ] INFO Application - The following 1 profile is active: "dev" -25-07-29.10:27:08.167 [main ] INFO RepositoryConfigurationDelegate - Multiple Spring Data modules found, entering strict repository configuration mode -25-07-29.10:27:08.171 [main ] INFO RepositoryConfigurationDelegate - Bootstrapping Spring Data Redis repositories in DEFAULT mode. -25-07-29.10:27:08.202 [main ] INFO RepositoryConfigurationDelegate - Finished Spring Data repository scanning in 14 ms. Found 0 Redis repository interfaces. -25-07-29.10:27:08.986 [main ] INFO TomcatWebServer - Tomcat initialized with port 8095 (http) -25-07-29.10:27:08.998 [main ] INFO Http11NioProtocol - Initializing ProtocolHandler ["http-nio-8095"] -25-07-29.10:27:09.000 [main ] INFO StandardService - Starting service [Tomcat] -25-07-29.10:27:09.000 [main ] INFO StandardEngine - Starting Servlet engine: [Apache Tomcat/10.1.19] -25-07-29.10:27:09.047 [main ] INFO [/] - Initializing Spring embedded WebApplicationContext -25-07-29.10:27:09.048 [main ] INFO ServletWebServerApplicationContext - Root WebApplicationContext: initialization completed in 1672 ms -25-07-29.10:27:09.446 [main ] INFO HikariDataSource - HikariCP - Starting... -25-07-29.10:27:09.630 [main ] INFO HikariPool - HikariCP - Added connection org.postgresql.jdbc.PgConnection@374b6e33 -25-07-29.10:27:09.632 [main ] INFO HikariDataSource - HikariCP - Start completed. -25-07-29.10:27:10.270 [main ] INFO Version - Redisson 3.44.0 -25-07-29.10:27:10.505 [redisson-netty-1-4] INFO ConnectionsHolder - 1 connections initialized for 192.168.10.218/192.168.10.218:26379 -25-07-29.10:27:10.546 [redisson-netty-1-13] INFO ConnectionsHolder - 5 connections initialized for 192.168.10.218/192.168.10.218:26379 -25-07-29.10:27:11.294 [main ] INFO Http11NioProtocol - Starting ProtocolHandler ["http-nio-8095"] -25-07-29.10:27:11.308 [main ] INFO TomcatWebServer - Tomcat started on port 8095 (http) with context path '' -25-07-29.10:27:11.324 [main ] INFO Application - Started Application in 4.858 seconds (process running for 5.691) -25-07-29.10:27:27.925 [http-nio-8095-exec-1] INFO [/] - Initializing Spring DispatcherServlet 'dispatcherServlet' -25-07-29.10:27:27.925 [http-nio-8095-exec-1] INFO DispatcherServlet - Initializing Servlet 'dispatcherServlet' -25-07-29.10:27:27.927 [http-nio-8095-exec-1] INFO DispatcherServlet - Completed initialization in 1 ms -25-07-29.10:27:27.977 [http-nio-8095-exec-1] INFO RAGController - 上传知识库开始:测试01 -25-07-29.10:27:28.578 [http-nio-8095-exec-1] INFO TextSplitter - Splitting up document into 2 chunks. -25-07-29.10:27:29.993 [http-nio-8095-exec-1] INFO TextSplitter - Splitting up document into 15 chunks. -25-07-29.10:27:43.442 [http-nio-8095-exec-1] INFO TextSplitter - Splitting up document into 6 chunks. -25-07-29.10:27:48.303 [http-nio-8095-exec-1] INFO TextSplitter - Splitting up document into 13 chunks. -25-07-29.10:27:59.867 [http-nio-8095-exec-1] INFO TextSplitter - Splitting up document into 5 chunks. -25-07-29.10:28:03.851 [http-nio-8095-exec-1] INFO TextSplitter - Splitting up document into 18 chunks. -25-07-29.10:28:20.184 [http-nio-8095-exec-1] INFO TextSplitter - Splitting up document into 2 chunks. -25-07-29.10:28:22.057 [http-nio-8095-exec-1] INFO RAGController - 上传知识库完成:测试01 -25-07-29.10:37:27.151 [SpringApplicationShutdownHook] INFO HikariDataSource - HikariCP - Shutdown initiated... -25-07-29.10:37:27.153 [SpringApplicationShutdownHook] INFO HikariDataSource - HikariCP - Shutdown completed. -25-07-29.10:37:37.830 [main ] INFO Application - Starting Application v1.0 using Java 17.0.2 with PID 1 (/app.jar started by root in /) -25-07-29.10:37:37.835 [main ] INFO Application - The following 1 profile is active: "dev" -25-07-29.10:37:38.660 [main ] INFO RepositoryConfigurationDelegate - Multiple Spring Data modules found, entering strict repository configuration mode -25-07-29.10:37:38.662 [main ] INFO RepositoryConfigurationDelegate - Bootstrapping Spring Data Redis repositories in DEFAULT mode. -25-07-29.10:37:38.689 [main ] INFO RepositoryConfigurationDelegate - Finished Spring Data repository scanning in 11 ms. Found 0 Redis repository interfaces. -25-07-29.10:37:39.369 [main ] INFO TomcatWebServer - Tomcat initialized with port 8095 (http) -25-07-29.10:37:39.378 [main ] INFO Http11NioProtocol - Initializing ProtocolHandler ["http-nio-8095"] -25-07-29.10:37:39.380 [main ] INFO StandardService - Starting service [Tomcat] -25-07-29.10:37:39.380 [main ] INFO StandardEngine - Starting Servlet engine: [Apache Tomcat/10.1.19] -25-07-29.10:37:39.416 [main ] INFO [/] - Initializing Spring embedded WebApplicationContext -25-07-29.10:37:39.416 [main ] INFO ServletWebServerApplicationContext - Root WebApplicationContext: initialization completed in 1477 ms -25-07-29.10:37:39.774 [main ] INFO HikariDataSource - HikariCP - Starting... -25-07-29.10:37:39.967 [main ] INFO HikariPool - HikariCP - Added connection org.postgresql.jdbc.PgConnection@cdbe995 -25-07-29.10:37:39.969 [main ] INFO HikariDataSource - HikariCP - Start completed. -25-07-29.10:37:42.629 [main ] INFO Version - Redisson 3.44.0 -25-07-29.10:37:42.905 [redisson-netty-1-4] INFO ConnectionsHolder - 1 connections initialized for 192.168.10.218/192.168.10.218:26379 -25-07-29.10:37:42.940 [redisson-netty-1-13] INFO ConnectionsHolder - 5 connections initialized for 192.168.10.218/192.168.10.218:26379 -25-07-29.10:37:43.452 [main ] INFO Http11NioProtocol - Starting ProtocolHandler ["http-nio-8095"] -25-07-29.10:37:43.460 [main ] INFO TomcatWebServer - Tomcat started on port 8095 (http) with context path '' -25-07-29.10:37:43.468 [main ] INFO Application - Started Application in 6.441 seconds (process running for 7.065) -25-07-29.10:37:55.291 [http-nio-8095-exec-1] INFO [/] - Initializing Spring DispatcherServlet 'dispatcherServlet' -25-07-29.10:37:55.291 [http-nio-8095-exec-1] INFO DispatcherServlet - Initializing Servlet 'dispatcherServlet' -25-07-29.10:37:55.292 [http-nio-8095-exec-1] INFO DispatcherServlet - Completed initialization in 1 ms -25-07-29.10:37:55.337 [http-nio-8095-exec-1] INFO RAGController - 上传知识库开始:测试02 -25-07-29.10:37:55.992 [http-nio-8095-exec-1] INFO RAGController - 上传知识库完成:测试02 -25-07-29.10:49:25.780 [SpringApplicationShutdownHook] INFO HikariDataSource - HikariCP - Shutdown initiated... -25-07-29.10:49:25.783 [SpringApplicationShutdownHook] INFO HikariDataSource - HikariCP - Shutdown completed. diff --git a/docs/tag/v1.0/nginx/html/index.html b/docs/tag/v1.0/nginx/html/index.html index 95d9370..e07aacb 100644 --- a/docs/tag/v1.0/nginx/html/index.html +++ b/docs/tag/v1.0/nginx/html/index.html @@ -33,9 +33,28 @@ - +
+ + + + + + + +
diff --git a/docs/tag/v1.0/nginx/html/js/index.js b/docs/tag/v1.0/nginx/html/js/index.js index e0ce399..002361a 100644 --- a/docs/tag/v1.0/nginx/html/js/index.js +++ b/docs/tag/v1.0/nginx/html/js/index.js @@ -1,4 +1,4 @@ -// ===== index.js (modified to auto-refresh RAG list after upload) ===== +// ===== index.js (with delete RAG support & auto-refresh + upload menu toggle) ===== const chatArea = document.getElementById('chatArea'); const messageInput = document.getElementById('messageInput'); @@ -8,16 +8,20 @@ const chatList = document.getElementById('chatList'); const welcomeMessage = document.getElementById('welcomeMessage'); const toggleSidebarBtn = document.getElementById('toggleSidebar'); const sidebar = document.getElementById('sidebar'); + +// 新增:与 RAG 操作相关的 DOM +const ragSelect = document.getElementById('ragSelect'); +const deleteRagBtn = document.getElementById('deleteRagBtn'); +const refreshRagBtn = document.getElementById('refreshRagBtn'); + let currentEventSource = null; let currentChatId = null; /** ------------------------- - * RAG 列表加载(提升为顶层可复用) + * RAG 列表加载(可复用) * 支持可选预选值 preselectTag * ------------------------- */ function loadRagOptions(preselectTag) { - const ragSelect = document.getElementById('ragSelect'); - return fetch('/api/v1/rag/query_rag_tag_list') .then(response => response.json()) .then(data => { @@ -32,31 +36,143 @@ function loadRagOptions(preselectTag) { ragSelect.add(option); }); - // 可选:预选刚创建的 ragTag - if (preselectTag) { + // 可选:预选刚创建/删除后保留的 tag + if (preselectTag !== undefined) { const exists = Array.from(ragSelect.options).some(o => o.value === preselectTag); - if (!exists) { - ragSelect.add(new Option(`Rag:${preselectTag}`, preselectTag)); + if (exists || preselectTag === '') { + ragSelect.value = preselectTag; } - ragSelect.value = preselectTag; } } }) .catch(error => { console.error('获取知识库列表失败:', error); + }) + .finally(() => { + // 根据当前是否选中某个 RAG,决定删除按钮是否可点 + if (deleteRagBtn) { + deleteRagBtn.disabled = !ragSelect.value; + } }); } -// 获取知识库列表(首屏) +/** ------------------------- + * 广播 RAG 更新(删除/新增后用于同步其它页面) + * ------------------------- */ +function broadcastRagUpdate(optionalTag) { + try { + // A) postMessage:同源且 opener 存在时生效(比如从 index 打开的 upload 页) + if (window.opener && !window.opener.closed) { + window.opener.postMessage( + { type: 'RAG_LIST_REFRESH', ragTag: optionalTag }, + window.location.origin + ); + } + } catch (_) {} + + try { + // B) BroadcastChannel:同源标签页广播 + if ('BroadcastChannel' in window) { + const bc = new BroadcastChannel('rag-updates'); + bc.postMessage({ type: 'rag:updated', ragTag: optionalTag }); + bc.close(); + } + } catch (_) {} +} + +/** ------------------------- + * 删除当前选择的 RAG + * 调用后端 DELETE /api/v1/rag/knowledge/{ragTag} + * ------------------------- */ +async function deleteCurrentRag() { + const tag = ragSelect.value; + if (!tag) { + alert('请先从下拉框选择一个知识库。'); + return; + } + + const ok = confirm( + `确认删除知识库「${tag}」吗?\n此操作不可恢复,将删除向量库中该知识库的所有数据。` + ); + if (!ok) return; + + setDeleteBtnBusy(true); + + try { + const resp = await fetch(`/api/v1/rag/knowledge/${encodeURIComponent(tag)}`, { + method: 'DELETE' + }); + + const data = await resp.json().catch(() => ({})); + + if (resp.ok && data.code === '0000') { + alert('删除成功'); + + // 删除后刷新下拉框,并清空选择 + await loadRagOptions(''); // 传空字符串,强制清空选择 + // 广播给其它页面(比如已打开的 upload.html 或其他 index) + broadcastRagUpdate(); + } else { + throw new Error(data.info || `HTTP ${resp.status}`); + } + } catch (e) { + alert('删除失败:' + (e?.message || '未知错误')); + } finally { + setDeleteBtnBusy(false); + } +} + +/** ------------------------- + * 设置删除按钮忙碌状态 + * ------------------------- */ +function setDeleteBtnBusy(busy) { + if (!deleteRagBtn) return; + if (busy) { + deleteRagBtn.disabled = true; + deleteRagBtn.dataset.originalText = deleteRagBtn.textContent; + deleteRagBtn.textContent = '删除中…'; + } else { + deleteRagBtn.textContent = + deleteRagBtn.dataset.originalText || '🗑 删除'; + deleteRagBtn.disabled = !ragSelect.value; + } +} + +/** ------------------------- + * 事件绑定:刷新/删除/选择变化 + * ------------------------- */ +if (refreshRagBtn) { + refreshRagBtn.addEventListener('click', () => { + // 保留现有选择刷新(若后端删除了该tag,刷新后它自然消失) + const keep = ragSelect.value || undefined; + loadRagOptions(keep); + }); +} + +if (deleteRagBtn) { + deleteRagBtn.addEventListener('click', deleteCurrentRag); +} + +if (ragSelect) { + ragSelect.addEventListener('change', () => { + if (deleteRagBtn) { + deleteRagBtn.disabled = !ragSelect.value; + } + }); +} + +/** ------------------------- + * 首屏加载 RAG 列表 + * ------------------------- */ document.addEventListener('DOMContentLoaded', function () { loadRagOptions(); }); /** ------------------------- - * 接收来自 upload.html 的刷新通知 - * 方式 A:postMessage - * 方式 B:BroadcastChannel - * 兜底:页面从后台切回前台自动刷新 + * 接收来自其它页面的 RAG 刷新通知 + * A:postMessage + * B:BroadcastChannel + * C:页面激活时兜底刷新 * ------------------------- */ // A. postMessage(当 upload.html 有 opener 时) window.addEventListener('message', (event) => { @@ -67,7 +183,7 @@ window.addEventListener('message', (event) => { } }); -// B. BroadcastChannel(不依赖 opener,现代浏览器有效) +// B. BroadcastChannel(不依赖 opener) if ('BroadcastChannel' in window) { const bc = new BroadcastChannel('rag-updates'); bc.addEventListener('message', (event) => { @@ -79,19 +195,18 @@ if ('BroadcastChannel' in window) { } // C. 兜底:页面重新获得焦点/可见时刷新一次 -window.addEventListener('focus', () => loadRagOptions()); +window.addEventListener('focus', () => loadRagOptions(ragSelect.value || undefined)); document.addEventListener('visibilitychange', () => { - if (!document.hidden) loadRagOptions(); + if (!document.hidden) loadRagOptions(ragSelect.value || undefined); }); /** ------------------------- - * 聊天逻辑 + * 聊天逻辑(原有内容保留) * ------------------------- */ function createNewChat() { const chatId = Date.now().toString(); currentChatId = chatId; localStorage.setItem('currentChatId', chatId); - // 修改数据结构为包含name和messages的对象 localStorage.setItem(`chat_${chatId}`, JSON.stringify({ name: '新聊天', messages: [] @@ -125,7 +240,6 @@ function updateChatList() { let chatData = JSON.parse(localStorage.getItem(chatKey)); const chatId = chatKey.split('_')[1]; - // 数据迁移:将旧数组格式转换为新对象格式 if (Array.isArray(chatData)) { chatData = { name: `聊天 ${new Date(parseInt(chatId)).toLocaleDateString()}`, @@ -135,11 +249,14 @@ function updateChatList() { } const li = document.createElement('li'); - li.className = `chat-item flex items-center justify-between p-2 hover:bg-gray-100 rounded-lg cursor-pointer transition-colors ${chatId === currentChatId ? 'bg-blue-50' : ''}`; + li.className = + `chat-item flex items-center justify-between p-2 hover:bg-gray-100 rounded-lg cursor-pointer transition-colors ${chatId === currentChatId ? 'bg-blue-50' : ''}`; li.innerHTML = `
${chatData.name}
-
${new Date(parseInt(chatId)).toLocaleDateString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit' })}
+
+ ${new Date(parseInt(chatId)).toLocaleDateString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit' })} +
@@ -162,7 +279,6 @@ function updateChatList() { } let currentContextMenu = null; -// 优化后的上下文菜单 function showChatContextMenu(event, chatId) { event.stopPropagation(); closeContextMenu(); @@ -192,32 +308,27 @@ function showChatContextMenu(event, chatId) { document.body.appendChild(menu); currentContextMenu = menu; - // 点击外部关闭菜单 setTimeout(() => { document.addEventListener('click', closeContextMenu, { once: true }); }); } - function closeContextMenu() { if (currentContextMenu) { currentContextMenu.remove(); currentContextMenu = null; } } - function renameChat(chatId) { const chatKey = `chat_${chatId}`; const chatData = JSON.parse(localStorage.getItem(chatKey)); const currentName = chatData.name || `聊天 ${new Date(parseInt(chatId)).toLocaleString()}`; const newName = prompt('请输入新的聊天名称', currentName); - if (newName) { chatData.name = newName; localStorage.setItem(chatKey, JSON.stringify(chatData)); updateChatList(); } } - function loadChat(chatId) { currentChatId = chatId; localStorage.setItem('currentChatId', chatId); @@ -228,21 +339,19 @@ function loadChat(chatId) { }); updateChatList(); } - function clearChatArea() { chatArea.innerHTML = ''; welcomeMessage.style.display = 'flex'; } - function appendMessage(content, isAssistant = false, saveToStorage = true) { welcomeMessage.style.display = 'none'; const messageDiv = document.createElement('div'); - messageDiv.className = `max-w-4xl mx-auto mb-4 p-4 rounded-lg ${isAssistant ? 'bg-gray-100' : 'bg-white border'} markdown-body relative`; + messageDiv.className = + `max-w-4xl mx-auto mb-4 p-4 rounded-lg ${isAssistant ? 'bg-gray-100' : 'bg-white border'} markdown-body relative`; const renderedContent = DOMPurify.sanitize(marked.parse(content)); messageDiv.innerHTML = renderedContent; - // 添加复制按钮 const copyBtn = document.createElement('button'); copyBtn.className = 'absolute top-2 right-2 p-1 bg-gray-200 rounded-md text-xs'; copyBtn.textContent = '复制'; @@ -257,9 +366,10 @@ function appendMessage(content, isAssistant = false, saveToStorage = true) { chatArea.appendChild(messageDiv); chatArea.scrollTop = chatArea.scrollHeight; - // 仅在需要时保存到本地存储 if (saveToStorage && currentChatId) { - const chatData = JSON.parse(localStorage.getItem(`chat_${currentChatId}`) || '{"name": "新聊天", "messages": []}'); + const chatData = JSON.parse( + localStorage.getItem(`chat_${currentChatId}`) || '{"name": "新聊天", "messages": []}' + ); chatData.messages.push({ content, isAssistant }); localStorage.setItem(`chat_${currentChatId}`, JSON.stringify(chatData)); } @@ -270,8 +380,7 @@ function startEventStream(message) { currentEventSource.close(); } - // 组装流式接口 - const ragTag = document.getElementById('ragSelect').value; + const ragTag = ragSelect.value; const aiModelSelect = document.getElementById('aiModel'); const aiModelValue = aiModelSelect.value; // openai / ollama const aiModelModel = aiModelSelect.options[aiModelSelect.selectedIndex].getAttribute('model'); @@ -298,7 +407,6 @@ function startEventStream(message) { const newContent = data.result.output.content; accumulatedContent += newContent; - // 首次创建临时消息容器 if (!tempMessageDiv) { tempMessageDiv = document.createElement('div'); tempMessageDiv.className = 'max-w-4xl mx-auto mb-4 p-4 rounded-lg bg-gray-100 markdown-body relative'; @@ -306,7 +414,6 @@ function startEventStream(message) { welcomeMessage.style.display = 'none'; } - // 直接更新文本内容(先不解析 Markdown) tempMessageDiv.textContent = accumulatedContent; chatArea.scrollTop = chatArea.scrollHeight; } @@ -314,11 +421,9 @@ function startEventStream(message) { if (data.result?.output?.properties?.finishReason === 'STOP') { currentEventSource.close(); - // 流式传输完成后进行最终渲染 const finalContent = accumulatedContent; tempMessageDiv.innerHTML = DOMPurify.sanitize(marked.parse(finalContent)); - // 添加复制按钮 const copyBtn = document.createElement('button'); copyBtn.className = 'absolute top-2 right-2 p-1 bg-gray-200 rounded-md text-xs'; copyBtn.textContent = '复制'; @@ -330,9 +435,10 @@ function startEventStream(message) { }; tempMessageDiv.appendChild(copyBtn); - // 保存到本地存储 if (currentChatId) { - const chatData = JSON.parse(localStorage.getItem(`chat_${currentChatId}`) || '{"name": "新聊天", "messages": []}'); + const chatData = JSON.parse( + localStorage.getItem(`chat_${currentChatId}`) || '{"name": "新聊天", "messages": []}' + ); chatData.messages.push({ content: finalContent, isAssistant: true }); localStorage.setItem(`chat_${currentChatId}`, JSON.stringify(chatData)); } @@ -391,7 +497,7 @@ if (savedChatId) { loadChat(savedChatId); } -// Handle window resize for responsive design +// Responsive window.addEventListener('resize', () => { if (window.innerWidth > 768) { sidebar.classList.remove('-translate-x-full'); @@ -400,33 +506,70 @@ window.addEventListener('resize', () => { } }); -// Initial check for mobile devices if (window.innerWidth <= 768) { sidebar.classList.add('-translate-x-full'); } updateSidebarIcon(); -// 上传知识下拉菜单控制 -const uploadMenuButton = document.getElementById('uploadMenuButton'); -const uploadMenu = document.getElementById('uploadMenu'); +/** ------------------------- + * 上传知识下拉菜单(保持你现有 HTML 结构) + * - 点击按钮展开/收起菜单 + * - 点击外部区域/按下 Esc 关闭 + * - 点击菜单项后自动关闭 + * ------------------------- */ +(function initUploadMenu() { + const uploadMenuButton = document.getElementById('uploadMenuButton'); + const uploadMenu = document.getElementById('uploadMenu'); + if (!uploadMenuButton || !uploadMenu) return; -// 切换菜单显示 -uploadMenuButton.addEventListener('click', (e) => { - e.stopPropagation(); - uploadMenu.classList.toggle('hidden'); -}); + // 切换菜单显示/隐藏 + const toggleMenu = () => { + uploadMenu.classList.toggle('hidden'); + }; -// 点击外部区域关闭菜单 -document.addEventListener('click', (e) => { - if (!uploadMenu.contains(e.target) && e.target !== uploadMenuButton) { + // 显示菜单 + const openMenu = () => { + uploadMenu.classList.remove('hidden'); + }; + + // 隐藏菜单 + const closeMenu = () => { uploadMenu.classList.add('hidden'); - } -}); + }; -// 菜单项点击后关闭菜单 -document.querySelectorAll('#uploadMenu a').forEach(item => { - item.addEventListener('click', () => { - uploadMenu.classList.add('hidden'); + // 点击按钮展开/收起 + uploadMenuButton.addEventListener('click', (e) => { + e.stopPropagation(); + toggleMenu(); }); -}); + + // 键盘辅助:Enter/Space 展开,Esc 关闭 + uploadMenuButton.addEventListener('keydown', (e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + openMenu(); + } else if (e.key === 'Escape') { + closeMenu(); + } + }); + + // 点击菜单项后自动关闭 + uploadMenu.querySelectorAll('a').forEach(a => { + a.addEventListener('click', () => closeMenu()); + }); + + // 点击外部任意区域关闭 + document.addEventListener('click', (e) => { + if (!uploadMenu.classList.contains('hidden')) { + if (!uploadMenu.contains(e.target) && e.target !== uploadMenuButton) { + closeMenu(); + } + } + }); + + // Esc 关闭(全局) + document.addEventListener('keydown', (e) => { + if (e.key === 'Escape') closeMenu(); + }); +})(); diff --git a/pom.xml b/pom.xml index 3fcd7ec..5bfb3a1 100644 --- a/pom.xml +++ b/pom.xml @@ -18,7 +18,7 @@ 17 17 UTF-8 - 0.8.1 + 1.0.0-M6