7.29 增加接口:查询知识库包含哪些文件 优化前端界面显示
This commit is contained in:
parent
8914ef7393
commit
e09ec7c70f
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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()
|
||||
|
@ -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())
|
||||
? 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<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();
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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"
|
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
|
||||
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
|
||||
|
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 {
|
||||
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>
|
||||
<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="header">
|
||||
<h1>解析Git仓库</h1>
|
||||
<p class="subtitle">将Git仓库内容导入知识库</p>
|
||||
</div>
|
||||
|
||||
<form id="uploadForm" class="upload-form">
|
||||
<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 class="form-group">
|
||||
<input type="text" id="userName" placeholder="用户名" required>
|
||||
<label for="userName">用户名</label>
|
||||
<input type="text" id="userName" placeholder="您的Git用户名" required>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<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>
|
||||
<div id="status"></div>
|
||||
|
||||
<div id="status" class="status-message"></div>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
|
||||
<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>
|
@ -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>
|
||||
|
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 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 刷新通知
|
||||
* 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 = '<div class="text-gray-500 text-sm p-4">暂无聊天记录</div>';
|
||||
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 = `
|
||||
<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="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>
|
||||
`;
|
||||
// 使用 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 = `
|
||||
<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();
|
||||
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 = '<div class="text-red-500 text-sm p-4">加载聊天列表失败</div>';
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 消息显示相关函数 ====================
|
||||
|
@ -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") {
|
||||
|
Loading…
x
Reference in New Issue
Block a user