7.29 增加接口:查询知识库包含哪些文件 优化前端界面显示
This commit is contained in:
parent
8914ef7393
commit
e09ec7c70f
@ -2,6 +2,10 @@ server:
|
|||||||
port: 8095
|
port: 8095
|
||||||
|
|
||||||
spring:
|
spring:
|
||||||
|
servlet:
|
||||||
|
multipart:
|
||||||
|
max-file-size: 10MB
|
||||||
|
max-request-size: 200MB
|
||||||
datasource:
|
datasource:
|
||||||
driver-class-name: org.postgresql.Driver
|
driver-class-name: org.postgresql.Driver
|
||||||
username: postgres
|
username: postgres
|
||||||
|
@ -21,7 +21,7 @@ public class ChatController {
|
|||||||
|
|
||||||
private static final String CHAT_PREFIX = "chat:"; // chat:{chatId}
|
private static final String CHAT_PREFIX = "chat:"; // chat:{chatId}
|
||||||
private static final String META_PREFIX = "chat_meta:"; // chat_meta:{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 RedissonClient redisson;
|
||||||
private final ObjectMapper mapper;
|
private final ObjectMapper mapper;
|
||||||
|
@ -85,12 +85,16 @@ public class OllamaController implements IAiService {
|
|||||||
log.info("generate_stream_rag called!用户问题是:"+message);
|
log.info("generate_stream_rag called!用户问题是:"+message);
|
||||||
|
|
||||||
String SYSTEM_PROMPT = """
|
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!
|
1. 你必须使用DOCUMENTS中的知识内容进行回答,回答时要表现得像你本来就知道这些知识
|
||||||
DOCUMENTS:
|
2. 如果DOCUMENTS中没有相关信息,请直接回答"我不清楚该问题的答案"
|
||||||
{documents}
|
3. 所有回答必须使用中文
|
||||||
""";
|
4. 回答时保持专业、友好且简洁
|
||||||
|
|
||||||
|
知识库内容:
|
||||||
|
{documents}
|
||||||
|
""";
|
||||||
|
|
||||||
// 基于用户 message 检索 TopK 文档,并使用 ragTag 过滤标签
|
// 基于用户 message 检索 TopK 文档,并使用 ragTag 过滤标签
|
||||||
SearchRequest request = SearchRequest.builder()
|
SearchRequest request = SearchRequest.builder()
|
||||||
|
@ -17,6 +17,7 @@ import org.springframework.ai.vectorstore.SimpleVectorStore;
|
|||||||
import org.springframework.ai.vectorstore.pgvector.PgVectorStore;
|
import org.springframework.ai.vectorstore.pgvector.PgVectorStore;
|
||||||
import org.springframework.core.io.PathResource;
|
import org.springframework.core.io.PathResource;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
@ -25,7 +26,10 @@ import java.io.IOException;
|
|||||||
import java.nio.file.*;
|
import java.nio.file.*;
|
||||||
import java.nio.file.attribute.BasicFileAttributes;
|
import java.nio.file.attribute.BasicFileAttributes;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* RAG 服务控制器,实现 IRAGService 接口,提供知识库管理和检索相关的 HTTP 接口
|
* RAG 服务控制器,实现 IRAGService 接口,提供知识库管理和检索相关的 HTTP 接口
|
||||||
@ -52,6 +56,7 @@ public class RAGController implements IRAGService {
|
|||||||
// Redisson 客户端,用于操作 Redis 列表存储 RAG 标签
|
// Redisson 客户端,用于操作 Redis 列表存储 RAG 标签
|
||||||
private final RedissonClient redissonClient;
|
private final RedissonClient redissonClient;
|
||||||
|
|
||||||
|
private final JdbcTemplate jdbcTemplate; // 注入Spring JDBC
|
||||||
/**
|
/**
|
||||||
* 查询所有已上传的 RAG 标签列表
|
* 查询所有已上传的 RAG 标签列表
|
||||||
* GET /api/v1/rag/query_rag_tag_list
|
* 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 读取文档内容
|
* - 使用 Tika 读取文档内容
|
||||||
* - 进行文本切分并贴上 ragTag 元数据
|
* - 进行文本切分并贴上 ragTag 元数据
|
||||||
@ -97,52 +152,77 @@ public class RAGController implements IRAGService {
|
|||||||
@RequestParam(value = "filePath", required = false) List<String> filePaths) {
|
@RequestParam(value = "filePath", required = false) List<String> filePaths) {
|
||||||
|
|
||||||
log.info("上传知识库开始:{}", ragTag);
|
log.info("上传知识库开始:{}", ragTag);
|
||||||
|
log.info("待处理文件数量:{},传入路径数量:{}", files.size(), filePaths != null ? filePaths.size() : 0);
|
||||||
|
|
||||||
// 使用带索引的 for 循环,保证与 filePath 一一对应
|
// 使用带索引的 for 循环,保证与 filePath 一一对应
|
||||||
for (int i = 0; i < files.size(); i++) {
|
for (int i = 0; i < files.size(); i++) {
|
||||||
MultipartFile file = files.get(i);
|
MultipartFile file = files.get(i);
|
||||||
|
String originalFilename = file.getOriginalFilename();
|
||||||
|
|
||||||
// ===== 新增:相对路径(与前端传入顺序一致;缺失时回退到原始文件名)=====
|
// ===== 路径标准化处理 =====
|
||||||
final String relPath =
|
// 1. 获取原始路径(优先使用前端传入的路径,否则使用文件名)
|
||||||
(filePaths != null && i < filePaths.size() && filePaths.get(i) != null && !filePaths.get(i).isBlank())
|
String rawPath = (filePaths != null && i < filePaths.size() && filePaths.get(i) != null && !filePaths.get(i).isBlank())
|
||||||
? filePaths.get(i)
|
? filePaths.get(i)
|
||||||
: (file.getOriginalFilename() != null ? file.getOriginalFilename() : "");
|
: (originalFilename != null ? originalFilename : "");
|
||||||
|
|
||||||
// 可选:调试日志
|
// 2. 标准化路径格式(统一用 / 分隔符,去除开头多余的 ./ 或 /)
|
||||||
log.debug("接收文件:{},相对路径:{}", file.getOriginalFilename(), relPath);
|
String normalizedPath = rawPath
|
||||||
|
.replace("\\", "/") // 统一使用正斜杠
|
||||||
|
.replaceAll("^[/.]+", "") // 去除开头的 ./ 或 /
|
||||||
|
.replaceAll("/+", "/"); // 替换多个连续的 / 为单个
|
||||||
|
|
||||||
|
// 打印当前处理的文件信息(包含原始和标准化路径)
|
||||||
|
log.info("正在处理第 {} 个文件 - 原始文件名: {}, 标准化路径: {}",
|
||||||
|
i + 1,
|
||||||
|
originalFilename,
|
||||||
|
normalizedPath);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 读取上传文件,提取文档内容
|
// 读取上传文件,提取文档内容
|
||||||
|
log.debug("开始解析文件内容: {}", normalizedPath);
|
||||||
TikaDocumentReader documentReader = new TikaDocumentReader(file.getResource());
|
TikaDocumentReader documentReader = new TikaDocumentReader(file.getResource());
|
||||||
List<Document> documents = documentReader.get();
|
List<Document> documents = documentReader.get();
|
||||||
|
|
||||||
// 对文档进行 Token 拆分
|
// 对文档进行 Token 拆分
|
||||||
|
log.debug("开始拆分文件内容: {}", normalizedPath);
|
||||||
List<Document> documentSplitterList = tokenTextSplitter.apply(documents);
|
List<Document> documentSplitterList = tokenTextSplitter.apply(documents);
|
||||||
|
|
||||||
// 为原文档和拆分文档设置 ragTag 元数据 + 相对路径
|
// ===== 元数据设置(保留完整标准化路径)=====
|
||||||
|
// 1. 原始文档设置元数据
|
||||||
documents.forEach(doc -> {
|
documents.forEach(doc -> {
|
||||||
doc.getMetadata().put("knowledge", ragTag);
|
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 -> {
|
documentSplitterList.forEach(doc -> {
|
||||||
doc.getMetadata().put("knowledge", ragTag);
|
doc.getMetadata().put("knowledge", ragTag);
|
||||||
doc.getMetadata().put("path", relPath);
|
doc.getMetadata().put("path", normalizedPath);
|
||||||
|
doc.getMetadata().put("original_filename", originalFilename);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 存储拆分后的文档到 pgVectorStore
|
// 存储拆分后的文档到 pgVectorStore
|
||||||
|
log.debug("开始存储文件到向量数据库: {}", normalizedPath);
|
||||||
pgVectorStore.accept(documentSplitterList);
|
pgVectorStore.accept(documentSplitterList);
|
||||||
|
log.info("文件处理完成: {}", normalizedPath);
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("文件处理失败:{} - {}", file.getOriginalFilename(), e.getMessage(), e);
|
log.error("文件处理失败:{} - {}", normalizedPath, e.getMessage(), e);
|
||||||
|
// 可选:记录失败文件信息,但不中断整体流程
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新 Redis 标签列表,避免重复
|
// 更新 Redis 标签列表(避免重复)
|
||||||
RList<String> elements = redissonClient.getList("ragTag");
|
RList<String> elements = redissonClient.getList("ragTag");
|
||||||
if (!elements.contains(ragTag)) {
|
if (!elements.contains(ragTag)) {
|
||||||
elements.add(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();
|
return Response.<String>builder().code("0000").info("调用成功").build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -63,6 +63,21 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- ai-rag-knowledge-network
|
- 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:
|
networks:
|
||||||
ai-rag-knowledge-network:
|
ai-rag-knowledge-network:
|
||||||
driver: bridge
|
driver: bridge
|
||||||
|
@ -1,9 +1,2 @@
|
|||||||
# A. 测试 embeddings(nomic-embed-text)
|
#REM 测试查询知识库文件列表 - 将 "your-rag-tag" 替换为实际的知识库标签
|
||||||
curl -s http://localhost:11434/api/embeddings \
|
curl -X GET "http://localhost:8095/api/v1/rag/knowledge/files/test-01" -H "Content-Type: application/json"
|
||||||
-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"}'
|
|
7
docs/tag/v1.0/api/local-test.txt
Normal file
7
docs/tag/v1.0/api/local-test.txt
Normal 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';
|
@ -62,7 +62,21 @@ services:
|
|||||||
retries: 10
|
retries: 10
|
||||||
networks:
|
networks:
|
||||||
- ai-rag-knowledge-network
|
- 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:
|
networks:
|
||||||
ai-rag-knowledge-network:
|
ai-rag-knowledge-network:
|
||||||
driver: bridge
|
driver: bridge
|
||||||
|
199
docs/tag/v1.0/nginx/html/css/git.css
Normal file
199
docs/tag/v1.0/nginx/html/css/git.css
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
@ -84,3 +84,33 @@
|
|||||||
#uploadMenu a {
|
#uploadMenu a {
|
||||||
transition: background-color 0.2s ease;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ -3,144 +3,55 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>解析仓库</title>
|
<title>Git仓库解析 - AiRagKnowledge</title>
|
||||||
<style>
|
<link rel="stylesheet" href="css/git.css">
|
||||||
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>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h1>上传Git仓库</h1>
|
<div class="header">
|
||||||
<form id="uploadForm">
|
<h1>解析Git仓库</h1>
|
||||||
|
<p class="subtitle">将Git仓库内容导入知识库</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form id="uploadForm" class="upload-form">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<input type="text" id="repoUrl" placeholder="Git仓库地址" required>
|
<label for="repoUrl">仓库地址</label>
|
||||||
|
<input type="text" id="repoUrl" placeholder="https://github.com/username/repo.git" required>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<input type="text" id="userName" placeholder="用户名" required>
|
<label for="userName">用户名</label>
|
||||||
|
<input type="text" id="userName" placeholder="您的Git用户名" required>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<input type="password" id="token" placeholder="密码/Token" required>
|
<label for="token">访问凭证</label>
|
||||||
|
<input type="password" id="token" placeholder="密码/Personal Access Token" required>
|
||||||
|
<div class="hint">推荐使用具有repo权限的Personal Access Token</div>
|
||||||
</div>
|
</div>
|
||||||
<button type="submit">提交</button>
|
|
||||||
|
<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>
|
</form>
|
||||||
<div id="status"></div>
|
|
||||||
|
<div id="status" class="status-message"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="overlay" id="loadingOverlay">
|
<div class="overlay" id="loadingOverlay">
|
||||||
<div class="loading-spinner"></div>
|
<div class="loading-content">
|
||||||
|
<div class="loading-spinner"></div>
|
||||||
|
<div class="loading-text">正在解析仓库...</div>
|
||||||
|
<div class="progress-text" id="progressText"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script src="js/git.js"></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>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
@ -46,6 +46,15 @@
|
|||||||
↻
|
↻
|
||||||
</button>
|
</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(不可逆) -->
|
<!-- 删除当前选中的 RAG(不可逆) -->
|
||||||
<button id="deleteRagBtn"
|
<button id="deleteRagBtn"
|
||||||
class="px-3 py-2 rounded-lg border text-red-600 hover:bg-red-50 disabled:opacity-50"
|
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>
|
||||||
</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>
|
<script src="js/index.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
93
docs/tag/v1.0/nginx/html/js/git.js
Normal file
93
docs/tag/v1.0/nginx/html/js/git.js
Normal 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);
|
||||||
|
}
|
||||||
|
});
|
@ -12,12 +12,275 @@ const sidebar = document.getElementById('sidebar');
|
|||||||
const ragSelect = document.getElementById('ragSelect');
|
const ragSelect = document.getElementById('ragSelect');
|
||||||
const deleteRagBtn = document.getElementById('deleteRagBtn');
|
const deleteRagBtn = document.getElementById('deleteRagBtn');
|
||||||
const refreshRagBtn = document.getElementById('refreshRagBtn');
|
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 currentEventSource = null;
|
||||||
let currentChatId = localStorage.getItem('currentChatId') || null;
|
let currentChatId = localStorage.getItem('currentChatId') || null;
|
||||||
// 在文件顶部添加变量
|
// 在文件顶部添加变量
|
||||||
let shouldAutoRename = false;
|
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 刷新通知
|
||||||
|
* 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 存储相关函数 ====================
|
// ==================== Redis 存储相关函数 ====================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -79,10 +342,8 @@ async function deleteChat(chatId) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
if (currentChatId === chatId) {
|
// 直接刷新页面
|
||||||
await createNewChat();
|
window.location.reload();
|
||||||
}
|
|
||||||
updateChatList();
|
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`HTTP ${response.status}`);
|
throw new Error(`HTTP ${response.status}`);
|
||||||
}
|
}
|
||||||
@ -160,60 +421,85 @@ async function saveMessage(chatId, content, isAssistant) {
|
|||||||
* 更新聊天列表 UI
|
* 更新聊天列表 UI
|
||||||
*/
|
*/
|
||||||
async function updateChatList() {
|
async function updateChatList() {
|
||||||
chatList.innerHTML = '';
|
try {
|
||||||
const chats = await getAllChats();
|
// 先清空列表
|
||||||
|
chatList.innerHTML = '';
|
||||||
|
|
||||||
// 当前聊天置顶
|
// 获取最新的聊天列表
|
||||||
chats.sort((a, b) => {
|
const chats = await getAllChats();
|
||||||
if (a.chatId === currentChatId) return -1;
|
|
||||||
if (b.chatId === currentChatId) return 1;
|
|
||||||
return parseInt(b.chatId) - parseInt(a.chatId); // 按时间倒序
|
|
||||||
});
|
|
||||||
|
|
||||||
chats.forEach(chat => {
|
if (!chats || chats.length === 0) {
|
||||||
const li = document.createElement('li');
|
chatList.innerHTML = '<div class="text-gray-500 text-sm p-4">暂无聊天记录</div>';
|
||||||
li.className = `chat-item flex items-center justify-between p-2 hover:bg-gray-100 rounded-lg cursor-pointer transition-colors ${
|
return;
|
||||||
chat.chatId === currentChatId ? 'bg-blue-50' : ''
|
}
|
||||||
}`;
|
|
||||||
|
|
||||||
const date = new Date(parseInt(chat.chatId)).toLocaleDateString('zh-CN', {
|
// 当前聊天置顶
|
||||||
year: 'numeric',
|
chats.sort((a, b) => {
|
||||||
month: '2-digit',
|
if (a.chatId === currentChatId) return -1;
|
||||||
day: '2-digit'
|
if (b.chatId === currentChatId) return 1;
|
||||||
|
return parseInt(b.chatId) - parseInt(a.chatId); // 按时间倒序
|
||||||
});
|
});
|
||||||
|
|
||||||
li.innerHTML = `
|
// 使用 DocumentFragment 提高性能
|
||||||
<div class="flex-1">
|
const fragment = document.createDocumentFragment();
|
||||||
<div class="text-sm font-medium">${chat.name}</div>
|
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
// 事件绑定
|
chats.forEach(chat => {
|
||||||
li.querySelectorAll('button')[0].addEventListener('click', (e) => {
|
const li = document.createElement('li');
|
||||||
e.stopPropagation();
|
li.className = `chat-item flex items-center justify-between p-2 hover:bg-gray-100 rounded-lg cursor-pointer transition-colors ${
|
||||||
renameChat(chat.chatId);
|
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 = `
|
||||||
|
<div class="flex-1">
|
||||||
|
<div class="text-sm font-medium">${chat.name}</div>
|
||||||
|
<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="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>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// 事件绑定 - 使用类名选择器更明确
|
||||||
|
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();
|
chatList.appendChild(fragment);
|
||||||
deleteChat(chat.chatId);
|
|
||||||
});
|
|
||||||
|
|
||||||
li.addEventListener('click', () => loadChat(chat.chatId));
|
} catch (error) {
|
||||||
li.addEventListener('mouseenter', () => {
|
console.error('更新聊天列表失败:', error);
|
||||||
li.querySelector('.chat-actions').classList.remove('opacity-0');
|
chatList.innerHTML = '<div class="text-red-500 text-sm p-4">加载聊天列表失败</div>';
|
||||||
});
|
}
|
||||||
li.addEventListener('mouseleave', () => {
|
|
||||||
li.querySelector('.chat-actions').classList.add('opacity-0');
|
|
||||||
});
|
|
||||||
|
|
||||||
chatList.appendChild(li);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== 消息显示相关函数 ====================
|
// ==================== 消息显示相关函数 ====================
|
||||||
|
@ -334,12 +334,27 @@ form.addEventListener("submit", async (e) => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await axios.post(ENDPOINT, fd, {
|
const res = await axios.post(ENDPOINT, fd, {
|
||||||
|
timeout: 300000, // 5分钟超时(单位:毫秒)
|
||||||
onUploadProgress: (evt) => {
|
onUploadProgress: (evt) => {
|
||||||
if (!evt.total) return;
|
if (!evt.total) return;
|
||||||
const pct = Math.round((evt.loaded / evt.total) * 100);
|
const pct = Math.round((evt.loaded / evt.total) * 100);
|
||||||
progressBar.style.width = `${pct}%`;
|
progressBar.style.width = `${pct}%`;
|
||||||
progressText.textContent = `${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") {
|
if (res?.data?.code === "0000") {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user