diff --git a/ai-rag-knowledge-app/src/main/resources/application-dev.yml b/ai-rag-knowledge-app/src/main/resources/application-dev.yml index 2dd8a1e..f649683 100644 --- a/ai-rag-knowledge-app/src/main/resources/application-dev.yml +++ b/ai-rag-knowledge-app/src/main/resources/application-dev.yml @@ -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 diff --git a/ai-rag-knowledge-trigger/src/main/java/edu/whut/trigger/http/ChatController.java b/ai-rag-knowledge-trigger/src/main/java/edu/whut/trigger/http/ChatController.java index f704b59..fba29f0 100644 --- a/ai-rag-knowledge-trigger/src/main/java/edu/whut/trigger/http/ChatController.java +++ b/ai-rag-knowledge-trigger/src/main/java/edu/whut/trigger/http/ChatController.java @@ -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); //整个聊天会话(chatId)30天有效期 private final RedissonClient redisson; private final ObjectMapper mapper; diff --git a/ai-rag-knowledge-trigger/src/main/java/edu/whut/trigger/http/OllamaController.java b/ai-rag-knowledge-trigger/src/main/java/edu/whut/trigger/http/OllamaController.java index 56b9cef..c57a941 100644 --- a/ai-rag-knowledge-trigger/src/main/java/edu/whut/trigger/http/OllamaController.java +++ b/ai-rag-knowledge-trigger/src/main/java/edu/whut/trigger/http/OllamaController.java @@ -85,12 +85,16 @@ 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: - {documents} - """; + 请严格遵循以下要求回答用户问题: + + 1. 你必须使用DOCUMENTS中的知识内容进行回答,回答时要表现得像你本来就知道这些知识 + 2. 如果DOCUMENTS中没有相关信息,请直接回答"我不清楚该问题的答案" + 3. 所有回答必须使用中文 + 4. 回答时保持专业、友好且简洁 + + 知识库内容: + {documents} + """; // 基于用户 message 检索 TopK 文档,并使用 ragTag 过滤标签 SearchRequest request = SearchRequest.builder() diff --git a/ai-rag-knowledge-trigger/src/main/java/edu/whut/trigger/http/RAGController.java b/ai-rag-knowledge-trigger/src/main/java/edu/whut/trigger/http/RAGController.java index d44f754..f032cb5 100644 --- a/ai-rag-knowledge-trigger/src/main/java/edu/whut/trigger/http/RAGController.java +++ b/ai-rag-knowledge-trigger/src/main/java/edu/whut/trigger/http/RAGController.java @@ -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> 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 filePaths = jdbcTemplate.queryForList( + sql, + new Object[]{ragTag}, + String.class + ); + + log.info("查询知识库文件列表完成:{},共 {} 个文件", ragTag, filePaths.size()); + return Response.>builder() + .code("0000") + .info("查询成功") + .data(filePaths) + .build(); + + } catch (Exception e) { + log.error("查询知识库文件列表失败:{}", ragTag, e); + return Response.>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 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()) - ? filePaths.get(i) - : (file.getOriginalFilename() != null ? file.getOriginalFilename() : ""); + // ===== 路径标准化处理 ===== + // 1. 获取原始路径(优先使用前端传入的路径,否则使用文件名) + String rawPath = (filePaths != null && i < filePaths.size() && filePaths.get(i) != null && !filePaths.get(i).isBlank()) + ? filePaths.get(i) + : (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 documents = documentReader.get(); // 对文档进行 Token 拆分 + log.debug("开始拆分文件内容: {}", normalizedPath); List 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 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.builder().code("0000").info("调用成功").build(); } diff --git a/docs/rag-dev-ops/docker-compose-environment-aliyun.yml b/docs/rag-dev-ops/docker-compose-environment-aliyun.yml index 03d5779..bb11388 100644 --- a/docs/rag-dev-ops/docker-compose-environment-aliyun.yml +++ b/docs/rag-dev-ops/docker-compose-environment-aliyun.yml @@ -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 diff --git a/docs/tag/v1.0/api/curl.sh b/docs/tag/v1.0/api/curl.sh index 8f4636c..06fe70e 100644 --- a/docs/tag/v1.0/api/curl.sh +++ b/docs/tag/v1.0/api/curl.sh @@ -1,9 +1,2 @@ -# A. 测试 embeddings(nomic-embed-text) -curl -s http://localhost:11434/api/embeddings \ - -H "Content-Type: application/json" \ - -d '{"model":"nomic-embed-text","input":"hello"}' - -# B. 测试 generate(deepseek-r1:1.5b) -curl -s http://localhost:11434/api/generate \ - -H "Content-Type: application/json" \ - -d '{"model":"deepseek-r1:1.5b","prompt":"say hi"}' +#REM 测试查询知识库文件列表 - 将 "your-rag-tag" 替换为实际的知识库标签 +curl -X GET "http://localhost:8095/api/v1/rag/knowledge/files/test-01" -H "Content-Type: application/json" \ No newline at end of file diff --git a/docs/tag/v1.0/api/local-test.txt b/docs/tag/v1.0/api/local-test.txt new file mode 100644 index 0000000..c6bd94e --- /dev/null +++ b/docs/tag/v1.0/api/local-test.txt @@ -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'; \ No newline at end of file diff --git a/docs/tag/v1.0/docker-compose-environment-aliyun.yml b/docs/tag/v1.0/docker-compose-environment-aliyun.yml index c75f346..cacfe24 100644 --- a/docs/tag/v1.0/docker-compose-environment-aliyun.yml +++ b/docs/tag/v1.0/docker-compose-environment-aliyun.yml @@ -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 diff --git a/docs/tag/v1.0/nginx/html/css/git.css b/docs/tag/v1.0/nginx/html/css/git.css new file mode 100644 index 0000000..27f274d --- /dev/null +++ b/docs/tag/v1.0/nginx/html/css/git.css @@ -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; + } +} \ No newline at end of file diff --git a/docs/tag/v1.0/nginx/html/css/index.css b/docs/tag/v1.0/nginx/html/css/index.css index aa986ec..f1e2575 100644 --- a/docs/tag/v1.0/nginx/html/css/index.css +++ b/docs/tag/v1.0/nginx/html/css/index.css @@ -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; +} + diff --git a/docs/tag/v1.0/nginx/html/git.html b/docs/tag/v1.0/nginx/html/git.html index 8488912..e5c64e1 100644 --- a/docs/tag/v1.0/nginx/html/git.html +++ b/docs/tag/v1.0/nginx/html/git.html @@ -3,144 +3,55 @@ - 解析仓库 - + Git仓库解析 - AiRagKnowledge +
-

上传Git仓库

-
+
+

解析Git仓库

+

将Git仓库内容导入知识库

+
+ +
- + +
+
- + +
+
- + + +
推荐使用具有repo权限的Personal Access Token
- + +
+ + +
+ +
-
+ +
-
+
+
+
正在解析仓库...
+
+
- + \ No newline at end of file diff --git a/docs/tag/v1.0/nginx/html/index.html b/docs/tag/v1.0/nginx/html/index.html index 7a1f427..7d5830f 100644 --- a/docs/tag/v1.0/nginx/html/index.html +++ b/docs/tag/v1.0/nginx/html/index.html @@ -46,6 +46,15 @@ ↻ + + + + +
+
    + +
+
+
+ +
+ + diff --git a/docs/tag/v1.0/nginx/html/js/git.js b/docs/tag/v1.0/nginx/html/js/git.js new file mode 100644 index 0000000..7640fc8 --- /dev/null +++ b/docs/tag/v1.0/nginx/html/js/git.js @@ -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); + } +}); \ No newline at end of file diff --git a/docs/tag/v1.0/nginx/html/js/index.js b/docs/tag/v1.0/nginx/html/js/index.js index 17245a5..7be8a41 100644 --- a/docs/tag/v1.0/nginx/html/js/index.js +++ b/docs/tag/v1.0/nginx/html/js/index.js @@ -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 = '
  • 加载中...
  • '; + 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 = '
  • 该知识库中没有文件
  • '; + } + } else { + throw new Error(result.info || '查询失败'); + } + } catch (error) { + console.error('查询知识库文件失败:', error); + ragFilesList.innerHTML = `
  • ${error.message}
  • `; + } +} + +/** + * 隐藏知识库文件对话框 + */ +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 刷新通知 + * A:postMessage + * B:BroadcastChannel + * 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,60 +421,85 @@ async function saveMessage(chatId, content, isAssistant) { * 更新聊天列表 UI */ async function updateChatList() { - chatList.innerHTML = ''; - const chats = await getAllChats(); + try { + // 先清空列表 + chatList.innerHTML = ''; - // 当前聊天置顶 - chats.sort((a, b) => { - if (a.chatId === currentChatId) return -1; - if (b.chatId === currentChatId) return 1; - return parseInt(b.chatId) - parseInt(a.chatId); // 按时间倒序 - }); + // 获取最新的聊天列表 + const chats = await getAllChats(); - 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 ${ - chat.chatId === currentChatId ? 'bg-blue-50' : '' - }`; + if (!chats || chats.length === 0) { + chatList.innerHTML = '
    暂无聊天记录
    '; + return; + } - const date = new Date(parseInt(chat.chatId)).toLocaleDateString('zh-CN', { - year: 'numeric', - month: '2-digit', - day: '2-digit' + // 当前聊天置顶 + chats.sort((a, b) => { + if (a.chatId === currentChatId) return -1; + if (b.chatId === currentChatId) return 1; + return parseInt(b.chatId) - parseInt(a.chatId); // 按时间倒序 }); - li.innerHTML = ` -
    -
    ${chat.name}
    -
    ${date}
    -
    -
    - - -
    - `; + // 使用 DocumentFragment 提高性能 + const fragment = document.createDocumentFragment(); - // 事件绑定 - li.querySelectorAll('button')[0].addEventListener('click', (e) => { - e.stopPropagation(); - renameChat(chat.chatId); + 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 ${ + chat.chatId === currentChatId ? 'bg-blue-50' : '' + }`; + + const date = new Date(parseInt(chat.chatId)).toLocaleDateString('zh-CN', { + year: 'numeric', + month: '2-digit', + day: '2-digit' + }); + + li.innerHTML = ` +
    +
    ${chat.name}
    +
    ${date}
    +
    +
    + + +
    + `; + + // 事件绑定 - 使用类名选择器更明确 + const renameBtn = li.querySelector('.rename-btn'); + const deleteBtn = li.querySelector('.delete-btn'); + + renameBtn.addEventListener('click', (e) => { + e.stopPropagation(); + renameChat(chat.chatId); + }); + + deleteBtn.addEventListener('click', (e) => { + e.stopPropagation(); + deleteChat(chat.chatId); + }); + + li.addEventListener('click', () => loadChat(chat.chatId)); + li.addEventListener('mouseenter', () => { + const actions = li.querySelector('.chat-actions'); + if (actions) actions.classList.remove('opacity-0'); + }); + li.addEventListener('mouseleave', () => { + const actions = li.querySelector('.chat-actions'); + if (actions) actions.classList.add('opacity-0'); + }); + + fragment.appendChild(li); }); - li.querySelectorAll('button')[1].addEventListener('click', (e) => { - e.stopPropagation(); - deleteChat(chat.chatId); - }); + // 一次性添加所有元素 + chatList.appendChild(fragment); - li.addEventListener('click', () => loadChat(chat.chatId)); - li.addEventListener('mouseenter', () => { - li.querySelector('.chat-actions').classList.remove('opacity-0'); - }); - li.addEventListener('mouseleave', () => { - li.querySelector('.chat-actions').classList.add('opacity-0'); - }); - - chatList.appendChild(li); - }); + } catch (error) { + console.error('更新聊天列表失败:', error); + chatList.innerHTML = '
    加载聊天列表失败
    '; + } } // ==================== 消息显示相关函数 ==================== diff --git a/docs/tag/v1.0/nginx/html/js/upload.js b/docs/tag/v1.0/nginx/html/js/upload.js index 4549850..5a0d2cb 100644 --- a/docs/tag/v1.0/nginx/html/js/upload.js +++ b/docs/tag/v1.0/nginx/html/js/upload.js @@ -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") {