7.29 增加接口:查询知识库包含哪些文件 优化前端界面显示

This commit is contained in:
zhangsan 2025-07-29 20:13:09 +08:00
parent 8914ef7393
commit e09ec7c70f
15 changed files with 886 additions and 203 deletions

View File

@ -2,6 +2,10 @@ server:
port: 8095
spring:
servlet:
multipart:
max-file-size: 10MB
max-request-size: 200MB
datasource:
driver-class-name: org.postgresql.Driver
username: postgres

View File

@ -21,7 +21,7 @@ public class ChatController {
private static final String CHAT_PREFIX = "chat:"; // chat:{chatId}
private static final String META_PREFIX = "chat_meta:"; // chat_meta:{chatId}
private static final Duration TTL = Duration.ofDays(30);
private static final Duration TTL = Duration.ofDays(30); //整个聊天会话chatId30天有效期
private final RedissonClient redisson;
private final ObjectMapper mapper;

View File

@ -85,10 +85,14 @@ public class OllamaController implements IAiService {
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!
DOCUMENTS:
请严格遵循以下要求回答用户问题
1. 你必须使用DOCUMENTS中的知识内容进行回答回答时要表现得像你本来就知道这些知识
2. 如果DOCUMENTS中没有相关信息请直接回答"我不清楚该问题的答案"
3. 所有回答必须使用中文
4. 回答时保持专业友好且简洁
知识库内容
{documents}
""";

View File

@ -17,6 +17,7 @@ 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.jdbc.core.JdbcTemplate;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
@ -25,7 +26,10 @@ 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;
import java.util.Objects;
import java.util.stream.Collectors;
/**
* RAG 服务控制器实现 IRAGService 接口提供知识库管理和检索相关的 HTTP 接口
@ -52,6 +56,7 @@ public class RAGController implements IRAGService {
// Redisson 客户端用于操作 Redis 列表存储 RAG 标签
private final RedissonClient redissonClient;
private final JdbcTemplate jdbcTemplate; // 注入Spring JDBC
/**
* 查询所有已上传的 RAG 标签列表
* GET /api/v1/rag/query_rag_tag_list
@ -83,6 +88,56 @@ public class RAGController implements IRAGService {
}
/**
* 查询指定知识库下的所有文件名
* GET /api/v1/rag/knowledge/{ragTag}/files
*/
@GetMapping("knowledge/files/{ragTag}")
public Response<List<String>> getKnowledgeFiles(@PathVariable String ragTag) {
log.info("查询知识库文件列表开始:{}", ragTag);
try {
// 使用原生SQL查询假设表名为 vector_store
String sql = """
SELECT DISTINCT metadata->>'path' as file_path
FROM vector_store
WHERE metadata->>'knowledge' = ?
ORDER BY metadata->>'path'
""";
// 执行查询并获取结果
List<String> filePaths = jdbcTemplate.queryForList(
sql,
new Object[]{ragTag},
String.class
);
log.info("查询知识库文件列表完成:{},共 {} 个文件", ragTag, filePaths.size());
return Response.<List<String>>builder()
.code("0000")
.info("查询成功")
.data(filePaths)
.build();
} catch (Exception e) {
log.error("查询知识库文件列表失败:{}", ragTag, e);
return Response.<List<String>>builder()
.code("9999")
.info("查询失败:" + e.getMessage())
.data(Collections.emptyList())
.build();
}
}
/**
* 注意可以追加文件到相同的知识库中
* 上传文件到知识库
* - 使用 Tika 读取文档内容
* - 进行文本切分并贴上 ragTag 元数据
* - 存储到 pgVectorStore 并更新 Redis 标签列表
* POST /api/v1/rag/file/upload
*/
/**
* 注意可以追加文件到相同的知识库中
* 上传文件到知识库
* - 使用 Tika 读取文档内容
* - 进行文本切分并贴上 ragTag 元数据
@ -97,52 +152,77 @@ public class RAGController implements IRAGService {
@RequestParam(value = "filePath", required = false) List<String> filePaths) {
log.info("上传知识库开始:{}", ragTag);
log.info("待处理文件数量:{},传入路径数量:{}", files.size(), filePaths != null ? filePaths.size() : 0);
// 使用带索引的 for 循环保证与 filePath 一一对应
for (int i = 0; i < files.size(); i++) {
MultipartFile file = files.get(i);
String originalFilename = file.getOriginalFilename();
// ===== 新增相对路径与前端传入顺序一致缺失时回退到原始文件名=====
final String relPath =
(filePaths != null && i < filePaths.size() && filePaths.get(i) != null && !filePaths.get(i).isBlank())
// ===== 路径标准化处理 =====
// 1. 获取原始路径优先使用前端传入的路径否则使用文件名
String rawPath = (filePaths != null && i < filePaths.size() && filePaths.get(i) != null && !filePaths.get(i).isBlank())
? filePaths.get(i)
: (file.getOriginalFilename() != null ? file.getOriginalFilename() : "");
: (originalFilename != null ? originalFilename : "");
// 可选调试日志
log.debug("接收文件:{},相对路径:{}", file.getOriginalFilename(), relPath);
// 2. 标准化路径格式统一用 / 分隔符去除开头多余的 ./ /
String normalizedPath = rawPath
.replace("\\", "/") // 统一使用正斜杠
.replaceAll("^[/.]+", "") // 去除开头的 ./ /
.replaceAll("/+", "/"); // 替换多个连续的 / 为单个
// 打印当前处理的文件信息包含原始和标准化路径
log.info("正在处理第 {} 个文件 - 原始文件名: {}, 标准化路径: {}",
i + 1,
originalFilename,
normalizedPath);
try {
// 读取上传文件提取文档内容
log.debug("开始解析文件内容: {}", normalizedPath);
TikaDocumentReader documentReader = new TikaDocumentReader(file.getResource());
List<Document> documents = documentReader.get();
// 对文档进行 Token 拆分
log.debug("开始拆分文件内容: {}", normalizedPath);
List<Document> documentSplitterList = tokenTextSplitter.apply(documents);
// 为原文档和拆分文档设置 ragTag 元数据 + 相对路径
// ===== 元数据设置保留完整标准化路径=====
// 1. 原始文档设置元数据
documents.forEach(doc -> {
doc.getMetadata().put("knowledge", ragTag);
doc.getMetadata().put("path", relPath);
doc.getMetadata().put("path", normalizedPath); // 使用标准化路径
doc.getMetadata().put("original_filename", originalFilename); // 可选额外保留原始文件名
});
// 2. 拆分后的文档设置元数据
documentSplitterList.forEach(doc -> {
doc.getMetadata().put("knowledge", ragTag);
doc.getMetadata().put("path", relPath);
doc.getMetadata().put("path", normalizedPath);
doc.getMetadata().put("original_filename", originalFilename);
});
// 存储拆分后的文档到 pgVectorStore
log.debug("开始存储文件到向量数据库: {}", normalizedPath);
pgVectorStore.accept(documentSplitterList);
log.info("文件处理完成: {}", normalizedPath);
} catch (Exception e) {
log.error("文件处理失败:{} - {}", file.getOriginalFilename(), e.getMessage(), e);
log.error("文件处理失败:{} - {}", normalizedPath, e.getMessage(), e);
// 可选记录失败文件信息但不中断整体流程
}
}
// 更新 Redis 标签列表避免重复
// 更新 Redis 标签列表避免重复
RList<String> elements = redissonClient.getList("ragTag");
if (!elements.contains(ragTag)) {
elements.add(ragTag);
log.info("新增知识库标签: {}", ragTag);
} else {
log.info("知识库标签已存在,无需新增: {}", ragTag);
}
log.info("上传知识库完成:{}", ragTag);
log.info("上传知识库完成:{},共处理 {} 个文件", ragTag, files.size());
return Response.<String>builder().code("0000").info("调用成功").build();
}

View File

@ -63,6 +63,21 @@ services:
networks:
- ai-rag-knowledge-network
# pg 管理工具
pgadmin:
image: registry.cn-hangzhou.aliyuncs.com/xfg-studio/pgadmin4:9.1.0
container_name: vector_db_admin
restart: always
ports:
- "5050:80"
environment:
PGADMIN_DEFAULT_EMAIL: admin@qq.com
PGADMIN_DEFAULT_PASSWORD: admin
depends_on:
- vector_db
networks:
- my-network
networks:
ai-rag-knowledge-network:
driver: bridge

View File

@ -1,9 +1,2 @@
# 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"}'
#REM 测试查询知识库文件列表 - 将 "your-rag-tag" 替换为实际的知识库标签
curl -X GET "http://localhost:8095/api/v1/rag/knowledge/files/test-01" -H "Content-Type: application/json"

View File

@ -0,0 +1,7 @@
TRUNCATE TABLE vector_store RESTART IDENTITY CASCADE; #清空向量表
-- PostgreSQL 示例(根据实际表结构调整)
SELECT metadata->>'path', COUNT(*) #查询知识库中包含哪些文件
FROM vector_store
WHERE metadata->>'knowledge' = 'test-08'
GROUP BY metadata->>'path';

View File

@ -62,7 +62,21 @@ services:
retries: 10
networks:
- ai-rag-knowledge-network
# pg 管理工具
pgadmin:
image: registry.cn-hangzhou.aliyuncs.com/xfg-studio/pgadmin4:9.1.0
container_name: vector_db_admin
restart: always
ports:
- "5050:80"
environment:
PGADMIN_DEFAULT_EMAIL: admin@qq.com
PGADMIN_DEFAULT_PASSWORD: admin
depends_on:
- vector_db
networks:
- my-network
-
networks:
ai-rag-knowledge-network:
driver: bridge

View File

@ -0,0 +1,199 @@
/* 基础样式 */
:root {
--primary-color: #2563eb;
--primary-hover: #1d4ed8;
--text-color: #333;
--light-gray: #f5f5f5;
--border-color: #ddd;
--error-color: #dc2626;
--success-color: #16a34a;
}
body {
font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif;
background-color: #f8fafc;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
margin: 0;
color: var(--text-color);
line-height: 1.6;
}
/* 容器样式 */
.container {
background-color: white;
padding: 2.5rem;
border-radius: 12px;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.08);
width: 100%;
max-width: 480px;
margin: 1rem;
}
.header {
text-align: center;
margin-bottom: 2rem;
}
.header h1 {
color: var(--primary-color);
margin: 0 0 0.5rem;
font-size: 1.8rem;
}
.subtitle {
color: #64748b;
margin: 0;
font-size: 0.95rem;
}
/* 表单样式 */
.upload-form {
display: flex;
flex-direction: column;
gap: 1.25rem;
}
.form-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.form-group label {
font-size: 0.95rem;
font-weight: 500;
color: #475569;
}
.form-group input {
width: 100%;
padding: 0.75rem 1rem;
border: 1px solid var(--border-color);
border-radius: 8px;
font-size: 1rem;
transition: border 0.2s;
}
.form-group input:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
}
.hint {
font-size: 0.8rem;
color: #64748b;
margin-top: 0.25rem;
}
/* 按钮样式 */
.submit-btn {
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
background-color: var(--primary-color);
color: white;
border: none;
padding: 0.75rem 1rem;
font-size: 1rem;
font-weight: 500;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
margin-top: 0.5rem;
}
.submit-btn:hover {
background-color: var(--primary-hover);
transform: translateY(-1px);
}
.submit-btn:active {
transform: translateY(0);
}
.btn-icon {
font-size: 1.2rem;
}
/* 状态消息 */
.status-message {
margin-top: 1.5rem;
padding: 0.75rem 1rem;
border-radius: 8px;
text-align: center;
font-weight: 500;
}
.status-message.error {
background-color: rgba(220, 38, 38, 0.1);
color: var(--error-color);
}
.status-message.success {
background-color: rgba(22, 163, 74, 0.1);
color: var(--success-color);
}
/* 加载遮罩 */
.overlay {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.7);
z-index: 1000;
justify-content: center;
align-items: center;
}
.loading-content {
background-color: white;
padding: 2rem;
border-radius: 12px;
text-align: center;
max-width: 300px;
width: 100%;
}
.loading-spinner {
border: 4px solid rgba(37, 99, 235, 0.1);
border-top: 4px solid var(--primary-color);
border-radius: 50%;
width: 50px;
height: 50px;
animation: spin 1s linear infinite;
margin: 0 auto 1rem;
}
.loading-text {
font-weight: 500;
margin-bottom: 0.5rem;
}
.progress-text {
font-size: 0.9rem;
color: #64748b;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* 响应式调整 */
@media (max-width: 500px) {
.container {
padding: 1.5rem;
}
.header h1 {
font-size: 1.5rem;
}
}

View File

@ -84,3 +84,33 @@
#uploadMenu a {
transition: background-color 0.2s ease;
}
/* 知识库文件对话框样式 */
#ragFilesModal {
transition: opacity 0.2s ease;
}
#ragFilesList li {
@apply p-2 hover:bg-gray-50 rounded flex items-center;
}
#ragFilesList li::before {
content: "📄";
@apply mr-2;
}
/* 对话框内容滚动条 */
#ragFilesModal div[class*="overflow-y-auto"] {
scrollbar-width: thin;
scrollbar-color: #cbd5e0 #f7fafc;
}
#ragFilesModal div[class*="overflow-y-auto"]::-webkit-scrollbar {
width: 6px;
}
#ragFilesModal div[class*="overflow-y-auto"]::-webkit-scrollbar-thumb {
background-color: #cbd5e0;
border-radius: 3px;
}

View File

@ -3,144 +3,55 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>解析仓库</title>
<style>
body {
font-family: 'Microsoft YaHei', '微软雅黑', sans-serif;
background-color: #f0f0f0;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
}
.container {
background-color: white;
padding: 2rem;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
text-align: center;
width: 300px;
}
h1 {
color: #333;
margin-bottom: 1.5rem;
}
.form-group {
margin-bottom: 1rem;
}
input {
width: 100%;
padding: 0.5rem;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 1rem;
box-sizing: border-box;
}
button {
background-color: #1E90FF;
color: white;
border: none;
padding: 0.5rem 1rem;
font-size: 1rem;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.3s;
width: 100%;
}
button:hover {
background-color: #4169E1;
}
#status {
margin-top: 1rem;
font-weight: bold;
}
.overlay {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
z-index: 1000;
justify-content: center;
align-items: center;
}
.loading-spinner {
border: 5px solid #f3f3f3;
border-top: 5px solid #3498db;
border-radius: 50%;
width: 50px;
height: 50px;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
<title>Git仓库解析 - AiRagKnowledge</title>
<link rel="stylesheet" href="css/git.css">
</head>
<body>
<div class="container">
<h1>上传Git仓库</h1>
<form id="uploadForm">
<div class="form-group">
<input type="text" id="repoUrl" placeholder="Git仓库地址" required>
<div class="header">
<h1>解析Git仓库</h1>
<p class="subtitle">将Git仓库内容导入知识库</p>
</div>
<form id="uploadForm" class="upload-form">
<div class="form-group">
<input type="text" id="userName" placeholder="用户名" required>
<label for="repoUrl">仓库地址</label>
<input type="text" id="repoUrl" placeholder="https://github.com/username/repo.git" required>
</div>
<div class="form-group">
<input type="password" id="token" placeholder="密码/Token" required>
<label for="userName">用户名</label>
<input type="text" id="userName" placeholder="您的Git用户名" required>
</div>
<button type="submit">提交</button>
<div class="form-group">
<label for="token">访问凭证</label>
<input type="password" id="token" placeholder="密码/Personal Access Token" required>
<div class="hint">推荐使用具有repo权限的Personal Access Token</div>
</div>
<div class="form-group">
<label for="ragTag">知识库标签</label>
<input type="text" id="ragTag" placeholder="自定义知识库名称" required>
</div>
<button type="submit" class="submit-btn">
<span class="btn-text">开始解析</span>
<span class="btn-icon"></span>
</button>
</form>
<div id="status"></div>
<div id="status" class="status-message"></div>
</div>
<div class="overlay" id="loadingOverlay">
<div class="loading-content">
<div class="loading-spinner"></div>
<div class="loading-text">正在解析仓库...</div>
<div class="progress-text" id="progressText"></div>
</div>
</div>
<script>
const loadingOverlay = document.getElementById('loadingOverlay');
document.getElementById('uploadForm').addEventListener('submit', function(e) {
e.preventDefault();
const repoUrl = document.getElementById('repoUrl').value;
const userName = document.getElementById('userName').value;
const token = document.getElementById('token').value;
loadingOverlay.style.display = 'flex';
document.getElementById('status').textContent = '';
fetch('/api/v1/rag/analyze_git_repository', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: `repoUrl=${encodeURIComponent(repoUrl)}&userName=${encodeURIComponent(userName)}&token=${encodeURIComponent(token)}`
})
.then(response => response.json())
.then(data => {
loadingOverlay.style.display = 'none';
if (data.code === '0000') {
document.getElementById('status').textContent = '上传成功';
// 成功提示并关闭窗口
setTimeout(() => {
alert('上传成功,窗口即将关闭');
window.close();
}, 500);
} else {
document.getElementById('status').textContent = '上传失败';
}
})
.catch(error => {
loadingOverlay.style.display = 'none';
document.getElementById('status').textContent = '上传仓库时出错';
});
});
</script>
<script src="js/git.js"></script>
</body>
</html>

View File

@ -46,6 +46,15 @@
</button>
<!-- 新增:查询知识库内容按钮 -->
<button id="queryRagBtn"
class="px-3 py-2 rounded-lg border bg-green-50 text-green-600 hover:bg-green-100 disabled:opacity-50"
title="查询知识库内容"
aria-label="查询知识库内容"
disabled>
🔍 查询
</button>
<!-- 删除当前选中的 RAG不可逆 -->
<button id="deleteRagBtn"
class="px-3 py-2 rounded-lg border text-red-600 hover:bg-red-50 disabled:opacity-50"
@ -134,6 +143,29 @@
</div>
</div>
</div>
<!-- 知识库文件查询对话框 -->
<div id="ragFilesModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden">
<div class="bg-white rounded-lg shadow-xl w-full max-w-2xl max-h-[80vh] flex flex-col">
<div class="p-4 border-b flex justify-between items-center">
<h3 class="text-lg font-semibold">知识库文件列表</h3>
<button id="closeRagFilesModal" class="p-1 hover:bg-gray-100 rounded-full">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div class="p-4 overflow-y-auto flex-1">
<ul id="ragFilesList" class="space-y-2">
<!-- 文件列表将通过JS动态填充 -->
</ul>
</div>
<div class="p-4 border-t text-right">
<button id="confirmRagFilesModal" class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">
关闭
</button>
</div>
</div>
</div>
<script src="js/index.js"></script>
</body>
</html>

View File

@ -0,0 +1,93 @@
document.addEventListener('DOMContentLoaded', function() {
const uploadForm = document.getElementById('uploadForm');
const loadingOverlay = document.getElementById('loadingOverlay');
const statusMessage = document.getElementById('status');
const progressText = document.getElementById('progressText');
// 表单提交处理
uploadForm.addEventListener('submit', async function(e) {
e.preventDefault();
// 获取表单数据
const repoUrl = document.getElementById('repoUrl').value.trim();
const userName = document.getElementById('userName').value.trim();
const token = document.getElementById('token').value.trim();
const ragTag = document.getElementById('ragTag').value.trim();
// 简单验证
if (!repoUrl || !userName || !token || !ragTag) {
showStatus('请填写所有必填字段', 'error');
return;
}
// 显示加载状态
showLoading();
statusMessage.textContent = '';
try {
// 发送请求到后端
const response = await fetch('/api/v1/rag/analyze_git_repository', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
repoUrl,
userName,
token,
ragTag
})
});
const data = await response.json();
if (response.ok && data.code === '0000') {
showStatus('仓库解析成功!', 'success');
// 成功提示并关闭窗口
setTimeout(() => {
alert('解析成功!知识库已更新。');
window.close();
}, 500);
} else {
throw new Error(data.info || '解析失败');
}
} catch (error) {
console.error('解析Git仓库出错:', error);
showStatus(`解析失败: ${error.message}`, 'error');
} finally {
hideLoading();
}
});
// 显示加载状态
function showLoading() {
loadingOverlay.style.display = 'flex';
updateProgress('正在连接Git仓库...');
}
// 隐藏加载状态
function hideLoading() {
loadingOverlay.style.display = 'none';
}
// 更新进度文本
function updateProgress(text) {
if (progressText) {
progressText.textContent = text;
}
}
// 显示状态消息
function showStatus(message, type = 'info') {
statusMessage.textContent = message;
statusMessage.className = 'status-message ' + type;
}
// 自动填充示例(开发时使用)
if (window.location.hostname === 'localhost') {
document.getElementById('repoUrl').value = 'https://github.com/username/repo.git';
document.getElementById('userName').value = 'your_username';
document.getElementById('ragTag').value = 'my-repo-' + new Date().toISOString().slice(0, 10);
}
});

View File

@ -12,12 +12,275 @@ const sidebar = document.getElementById('sidebar');
const ragSelect = document.getElementById('ragSelect');
const deleteRagBtn = document.getElementById('deleteRagBtn');
const refreshRagBtn = document.getElementById('refreshRagBtn');
const queryRagBtn = document.getElementById('queryRagBtn');
const ragFilesModal = document.getElementById('ragFilesModal');
const closeRagFilesModal = document.getElementById('closeRagFilesModal');
const confirmRagFilesModal = document.getElementById('confirmRagFilesModal');
const ragFilesList = document.getElementById('ragFilesList');
let currentEventSource = null;
let currentChatId = localStorage.getItem('currentChatId') || null;
// 在文件顶部添加变量
let shouldAutoRename = false;
/**
* 显示知识库文件对话框
*/
async function showRagFiles() {
const ragTag = ragSelect.value;
if (!ragTag) return;
try {
// 显示加载状态
ragFilesList.innerHTML = '<li class="text-center py-4 text-gray-500">加载中...</li>';
ragFilesModal.classList.remove('hidden');
// 调用API获取文件列表
const response = await fetch(`/api/v1/rag/knowledge/files/${ragTag}`);
const result = await response.json();
if (response.ok && result.code === '0000') {
// 清空并填充文件列表
ragFilesList.innerHTML = '';
if (result.data && result.data.length > 0) {
result.data.forEach(filePath => {
const li = document.createElement('li');
li.className = 'p-2 bg-gray-50 rounded border';
li.textContent = filePath;
ragFilesList.appendChild(li);
});
} else {
ragFilesList.innerHTML = '<li class="text-center py-4 text-gray-500">该知识库中没有文件</li>';
}
} else {
throw new Error(result.info || '查询失败');
}
} catch (error) {
console.error('查询知识库文件失败:', error);
ragFilesList.innerHTML = `<li class="text-center py-4 text-red-500">${error.message}</li>`;
}
}
/**
* 隐藏知识库文件对话框
*/
function hideRagFiles() {
ragFilesModal.classList.add('hidden');
}
/**
* 更新按钮状态
*/
function updateButtonStates() {
const hasRagSelected = ragSelect && ragSelect.value;
if (deleteRagBtn) {
deleteRagBtn.disabled = !hasRagSelected;
}
if (queryRagBtn) {
queryRagBtn.disabled = !hasRagSelected;
}
}
/** -------------------------
* RAG 列表加载可复用
* 支持可选预选值 preselectTag
* ------------------------- */
function loadRagOptions(preselectTag) {
return fetch('/api/v1/rag/query_rag_tag_list')
.then(response => response.json())
.then(data => {
if (data.code === '0000' && data.data) {
// 清空现有选项(保留第一个默认选项)
while (ragSelect.options.length > 1) {
ragSelect.remove(1);
}
// 添加新选项
data.data.forEach(tag => {
const option = new Option(`Rag${tag}`, tag);
ragSelect.add(option);
});
// 可选:预选刚创建/删除后保留的 tag
if (preselectTag !== undefined) {
const exists = Array.from(ragSelect.options).some(o => o.value === preselectTag);
if (exists || preselectTag === '') {
ragSelect.value = preselectTag;
}
}
}
})
.catch(error => {
console.error('获取知识库列表失败:', error);
})
.finally(() => {
updateButtonStates();
});
}
/** -------------------------
* 广播 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 || '🗑 删除';
updateButtonStates();
}
}
/** -------------------------
* 事件绑定刷新/删除/选择变化
* ------------------------- */
if (refreshRagBtn) {
refreshRagBtn.addEventListener('click', () => {
// 保留现有选择刷新若后端删除了该tag刷新后它自然消失
const keep = ragSelect.value || undefined;
loadRagOptions(keep);
});
}
if (deleteRagBtn) {
deleteRagBtn.addEventListener('click', deleteCurrentRag);
}
if (queryRagBtn) {
queryRagBtn.addEventListener('click', showRagFiles);
}
if (ragSelect) {
ragSelect.addEventListener('change', updateButtonStates);
}
if (closeRagFilesModal) {
closeRagFilesModal.addEventListener('click', hideRagFiles);
}
if (confirmRagFilesModal) {
confirmRagFilesModal.addEventListener('click', hideRagFiles);
}
// 点击外部关闭对话框
if (ragFilesModal) {
ragFilesModal.addEventListener('click', (e) => {
if (e.target === ragFilesModal) {
hideRagFiles();
}
});
}
/** -------------------------
* 首屏加载 RAG 列表
* ------------------------- */
document.addEventListener('DOMContentLoaded', function () {
loadRagOptions();
});
/** -------------------------
* 接收来自其它页面的 RAG 刷新通知
* ApostMessage
* BBroadcastChannel
* C页面激活时兜底刷新
* ------------------------- */
// A. postMessage当 upload.html 有 opener 时)
window.addEventListener('message', (event) => {
if (event.origin !== window.location.origin) return;
const msg = event.data;
if (msg && msg.type === 'RAG_LIST_REFRESH') {
loadRagOptions(msg.ragTag);
}
});
// B. BroadcastChannel不依赖 opener
if ('BroadcastChannel' in window) {
const bc = new BroadcastChannel('rag-updates');
bc.addEventListener('message', (event) => {
const msg = event.data;
if (msg && msg.type === 'rag:updated') {
loadRagOptions(msg.ragTag);
}
});
}
// C. 兜底:页面重新获得焦点/可见时刷新一次
window.addEventListener('focus', () => loadRagOptions(ragSelect.value || undefined));
document.addEventListener('visibilitychange', () => {
if (!document.hidden) loadRagOptions(ragSelect.value || undefined);
});
// ==================== Redis 存储相关函数 ====================
/**
@ -79,10 +342,8 @@ async function deleteChat(chatId) {
});
if (response.ok) {
if (currentChatId === chatId) {
await createNewChat();
}
updateChatList();
// 直接刷新页面
window.location.reload();
} else {
throw new Error(`HTTP ${response.status}`);
}
@ -160,9 +421,18 @@ async function saveMessage(chatId, content, isAssistant) {
* 更新聊天列表 UI
*/
async function updateChatList() {
try {
// 先清空列表
chatList.innerHTML = '';
// 获取最新的聊天列表
const chats = await getAllChats();
if (!chats || chats.length === 0) {
chatList.innerHTML = '<div class="text-gray-500 text-sm p-4">暂无聊天记录</div>';
return;
}
// 当前聊天置顶
chats.sort((a, b) => {
if (a.chatId === currentChatId) return -1;
@ -170,6 +440,9 @@ async function updateChatList() {
return parseInt(b.chatId) - parseInt(a.chatId); // 按时间倒序
});
// 使用 DocumentFragment 提高性能
const fragment = document.createDocumentFragment();
chats.forEach(chat => {
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 ${
@ -188,32 +461,45 @@ async function updateChatList() {
<div class="text-xs text-gray-400">${date}</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">重命名</button>
<button class="p-1 hover:bg-red-200 rounded text-red-500">删除</button>
<button class="rename-btn p-1 hover:bg-gray-200 rounded text-gray-500">重命名</button>
<button class="delete-btn p-1 hover:bg-red-200 rounded text-red-500">删除</button>
</div>
`;
// 事件绑定
li.querySelectorAll('button')[0].addEventListener('click', (e) => {
// 事件绑定 - 使用类名选择器更明确
const renameBtn = li.querySelector('.rename-btn');
const deleteBtn = li.querySelector('.delete-btn');
renameBtn.addEventListener('click', (e) => {
e.stopPropagation();
renameChat(chat.chatId);
});
li.querySelectorAll('button')[1].addEventListener('click', (e) => {
deleteBtn.addEventListener('click', (e) => {
e.stopPropagation();
deleteChat(chat.chatId);
});
li.addEventListener('click', () => loadChat(chat.chatId));
li.addEventListener('mouseenter', () => {
li.querySelector('.chat-actions').classList.remove('opacity-0');
const actions = li.querySelector('.chat-actions');
if (actions) actions.classList.remove('opacity-0');
});
li.addEventListener('mouseleave', () => {
li.querySelector('.chat-actions').classList.add('opacity-0');
const actions = li.querySelector('.chat-actions');
if (actions) actions.classList.add('opacity-0');
});
chatList.appendChild(li);
fragment.appendChild(li);
});
// 一次性添加所有元素
chatList.appendChild(fragment);
} catch (error) {
console.error('更新聊天列表失败:', error);
chatList.innerHTML = '<div class="text-red-500 text-sm p-4">加载聊天列表失败</div>';
}
}
// ==================== 消息显示相关函数 ====================

View File

@ -334,12 +334,27 @@ form.addEventListener("submit", async (e) => {
try {
const res = await axios.post(ENDPOINT, fd, {
timeout: 300000, // 5分钟超时单位毫秒
onUploadProgress: (evt) => {
if (!evt.total) return;
const pct = Math.round((evt.loaded / evt.total) * 100);
progressBar.style.width = `${pct}%`;
progressText.textContent = `${pct}%`;
// 可选:添加详细上传信息
console.log(`已上传: ${formatBytes(evt.loaded)} / ${formatBytes(evt.total)}`);
},
headers: {
'X-Request-Timeout': '300000', // 通知服务器期望的超时时间
'Content-Type': 'multipart/form-data' // 确保内容类型正确
}
}).catch(error => {
if (error.code === 'ECONNABORTED') {
console.error('请求超时,请尝试减少文件大小或分批上传');
} else {
console.error('上传错误:', error.message);
}
throw error; // 重新抛出错误以便外部捕获
});
if (res?.data?.code === "0000") {