7.29 增加删除知识库功能

This commit is contained in:
zhangsan 2025-07-29 13:18:59 +08:00
parent 81b71d0d07
commit 37b696e61b
16 changed files with 459 additions and 564 deletions

2
.gitignore vendored
View File

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

View File

@ -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<ChatResponse> generateStream(String model, String message);
Flux<ChatResponse> generateStreamRag(String model, String ragTag, String message);
}

View File

@ -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 的嵌入模型
* EmbeddingModelOllama使用配置项 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();
}
}

View File

@ -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<Document> 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<Document> original = reader.get();
List<Document> 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<Document> splits = tokenTextSplitter.apply(docs);
splits.forEach(d -> d.getMetadata().put("knowledge", "group-buy-market"));
// 6) 写入向量存储
pgVectorStore.accept(splits);
return FileVisitResult.CONTINUE;
}
});
}
}

View File

@ -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<Document> documents = reader.get();
// 对提取的文档进行 Token 拆分
List<Document> documentSplitterList = tokenTextSplitter.apply(documents);
List<Document> 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<Document> documents = pgVectorStore.similaritySearch(request);
// 拼接所有文档内容
String documentsCollectors = documents.stream()
.map(Document::getContent)
// 执行相似度搜索
List<Document> 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<Message> messages = new ArrayList<>();
// 发起对话
List<Message> 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));
}
}

View File

@ -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&lt;ChatResponse&gt;
* 示例: GET /generate_stream?model=deepseek-r1:1.5b&message=hi
* 流式生成接口返回 Flux<ChatResponse>
* 示例: GET /api/v1/ollama/generate_stream?model=deepseek-r1:1.5b&message=hi
*/
@GetMapping("generate_stream")
@Override
public Flux<ChatResponse> 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<Document> 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<Message> 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()
));
}
}

View File

@ -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<List<String>> queryRagTagList() {
// Redis 列表获取所有标签
RList<String> elements = redissonClient.getList("ragTag");
// 读一个快照便于安全日志与返回
List<String> 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.<List<String>>builder()
.code("0000")
.info("调用成功")
@ -81,7 +94,7 @@ public class RAGController implements IRAGService {
public Response<String> uploadFile(
@RequestParam("ragTag") String ragTag,
@RequestParam("file") List<MultipartFile> files,
@RequestParam(value = "filePath", required = false) List<String> filePaths ) {
@RequestParam(value = "filePath", required = false) List<String> 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<Document> documents = documentReader.get();
try {
// 读取上传文件提取文档内容
TikaDocumentReader documentReader = new TikaDocumentReader(file.getResource());
List<Document> documents = documentReader.get();
// 对文档进行 Token 拆分
List<Document> documentSplitterList = tokenTextSplitter.apply(documents);
// 对文档进行 Token 拆分
List<Document> 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<String> 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<String> elements = redissonClient.getList("ragTag");
if (!elements.contains(ragTag)) {
elements.add(ragTag);
}
log.info("上传知识库完成:{}", ragTag);
return Response.<String>builder().code("0000").info("调用成功").build();
}
/**
* 克隆并分析 Git 仓库
* - 克隆指定仓库到本地
@ -179,7 +192,7 @@ public class RAGController implements IRAGService {
// 2.2 复制为可变列表并过滤掉空内容
List<Document> 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.<String>builder().code("0000").info("调用成功").build();
}
/**
* 测试接口
* @return
*/
@GetMapping("knowledge/_ping")
public Response<String> ragPing() {
log.info("RAG knowledge ping");
return Response.<String>builder().code("0000").info("ok").build();
}
/**
* 删除指定知识库 ragTag
* - pgvector 向量库中删除所有 metadata.knowledge == ragTag 的文档
* - Redis ragTag 列表中删除该标签
* DELETE /api/v1/rag/knowledge/{ragTag}
*/
@DeleteMapping("knowledge/{ragTag}")
public Response<String> deleteKnowledge(@PathVariable("ragTag") String ragTag) {
log.info("删除知识库开始:{}", ragTag);
try {
// 1) 删除向量库中对应知识库的所有向量1.0.0-M6支持过滤表达式删除
pgVectorStore.delete("knowledge == '" + ragTag + "'");
// 2) Redis 标签列表移除
RList<String> elements = redissonClient.getList("ragTag");
elements.remove(ragTag);
log.info("删除知识库完成:{}", ragTag);
return Response.<String>builder().code("0000").info("删除成功").build();
} catch (Exception e) {
log.error("删除知识库失败:{}", ragTag, e);
return Response.<String>builder().code("9999").info("删除失败:" + e.getMessage()).build();
}
}
/**
* Git 仓库 URL 提取项目名称去除 .git 后缀
*/

View File

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

View File

@ -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') {
// 成功提示并关闭窗口

View File

@ -1,9 +1 @@
# A. 测试 embeddingsnomic-embed-text
curl -s http://localhost:11434/api/embeddings \
-H "Content-Type: application/json" \
-d '{"model":"nomic-embed-text","input":"hello"}'
# B. 测试 generatedeepseek-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

View File

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

View File

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

View File

@ -33,9 +33,28 @@
<option value="openai" model="gpt-4o">gpt-4o</option>
</select>
<select id="ragSelect" class="px-3 py-2 border rounded-lg flex-1 max-w-xs">
<option value="">选择一个知识库</option>
</select>
<div class="flex items-center gap-2">
<select id="ragSelect" class="px-3 py-2 border rounded-lg max-w-xs">
<option value="">选择一个知识库</option>
</select>
<!-- 手动刷新 RAG 列表 -->
<button id="refreshRagBtn"
class="px-2 py-2 rounded-lg border hover:bg-gray-100"
title="刷新知识库列表"
aria-label="刷新知识库列表">
</button>
<!-- 删除当前选中的 RAG不可逆 -->
<button id="deleteRagBtn"
class="px-3 py-2 rounded-lg border text-red-600 hover:bg-red-50 disabled:opacity-50"
title="删除所选知识库(不可恢复)"
aria-label="删除所选知识库"
disabled>
🗑 删除
</button>
</div>
</div>
<div class="flex items-center gap-2">

View File

@ -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 的刷新通知
* 方式 ApostMessage
* 方式 BBroadcastChannel
* 兜底页面从后台切回前台自动刷新
* 接收来自其它页面的 RAG 刷新通知
* ApostMessage
* BBroadcastChannel
* 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 = `
<div class="flex-1">
<div class="text-sm font-medium">${chatData.name}</div>
<div class="text-xs text-gray-400">${new Date(parseInt(chatId)).toLocaleDateString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit' })}</div>
<div class="text-xs text-gray-400">
${new Date(parseInt(chatId)).toLocaleDateString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit' })}
</div>
</div>
<div class="chat-actions flex items-center gap-1 opacity-0 transition-opacity duration-200">
<button class="p-1 hover:bg-gray-200 rounded text-gray-500" onclick="renameChat('${chatId}')">重命名</button>
@ -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();
});
})();

View File

@ -18,7 +18,7 @@
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<spring-ai.version>0.8.1</spring-ai.version>
<spring-ai.version>1.0.0-M6</spring-ai.version>
</properties>
<repositories>