feat(knowledge): 添加文档基本信息和内容信息的获取接口,支持文件下载功能
- 在知识库路由中新增获取文档基本信息和内容信息的接口 - 实现文档下载功能,支持中文文件名处理 - 更新相关文档和API调用,确保前端能够正确下载文件
This commit is contained in:
parent
c934ccb10d
commit
6d7e3ea2ef
@ -23,10 +23,10 @@
|
||||
|
||||
### ✨ 核心特性
|
||||
|
||||
- 🤖 **多模型支持** - 适配主流大模型平台及离线方案(vLLM、Ollama),支持自定义智能体开发,兼容 LangGraph 部署
|
||||
- 🤖 **模型与智能体** - 支持主流大模型及 vLLM、Ollama 等,支持自定义智能体开发,兼容 LangGraph 部署
|
||||
- 📚 **灵活知识库** - 支持 LightRAG、Milvus、Chroma 等存储形式,配置 MinerU、PP-Structure-V3 文档解析引擎
|
||||
- 🕸️ **知识图谱** - 支持 LightRAG 自动图谱构建,以及自定义图谱问答,可接入现有知识图谱
|
||||
- 👥 **权限安全** - 支持超级管理员、管理员、普通用户三级权限体系,并配置守卫模型
|
||||
- 👥 **权限安全** - 支持超级管理员、管理员、普通用户三级权限体系,并配置内容审查以及守卫模型
|
||||
|
||||
<div align="center">
|
||||
<!-- 视频缩略图 -->
|
||||
|
||||
@ -14,6 +14,7 @@
|
||||
- [ ] 添加统计信息
|
||||
- [x] 添加内容审查功能
|
||||
- [x] 补充 embedding 模型和 reranker 模型的配置说明
|
||||
- [ ] 知识库文件应该上传到 minio 中,然后就可以支持 从 minio 中下载文件
|
||||
|
||||
📝 **Base**
|
||||
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
import os
|
||||
import traceback
|
||||
from urllib.parse import quote, unquote
|
||||
|
||||
from fastapi import APIRouter, Body, Depends, File, HTTPException, Query, UploadFile
|
||||
from fastapi import APIRouter, Body, Depends, File, HTTPException, Query, Request, UploadFile
|
||||
from fastapi.responses import FileResponse
|
||||
from starlette.responses import FileResponse as StarletteFileResponse
|
||||
|
||||
from server.models.user_model import User
|
||||
from server.utils.auth_middleware import get_admin_user
|
||||
@ -160,7 +162,7 @@ async def add_documents(
|
||||
|
||||
@knowledge.get("/databases/{db_id}/documents/{doc_id}")
|
||||
async def get_document_info(db_id: str, doc_id: str, current_user: User = Depends(get_admin_user)):
|
||||
"""获取文档详细信息"""
|
||||
"""获取文档详细信息(包含基本信息和内容信息)"""
|
||||
logger.debug(f"GET document {doc_id} info in {db_id}")
|
||||
|
||||
try:
|
||||
@ -171,6 +173,32 @@ async def get_document_info(db_id: str, doc_id: str, current_user: User = Depend
|
||||
return {"message": "Failed to get file info", "status": "failed"}
|
||||
|
||||
|
||||
@knowledge.get("/databases/{db_id}/documents/{doc_id}/basic")
|
||||
async def get_document_basic_info(db_id: str, doc_id: str, current_user: User = Depends(get_admin_user)):
|
||||
"""获取文档基本信息(仅元数据)"""
|
||||
logger.debug(f"GET document {doc_id} basic info in {db_id}")
|
||||
|
||||
try:
|
||||
info = await knowledge_base.get_file_basic_info(db_id, doc_id)
|
||||
return info
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get file basic info, {e}, {db_id=}, {doc_id=}, {traceback.format_exc()}")
|
||||
return {"message": "Failed to get file basic info", "status": "failed"}
|
||||
|
||||
|
||||
@knowledge.get("/databases/{db_id}/documents/{doc_id}/content")
|
||||
async def get_document_content(db_id: str, doc_id: str, current_user: User = Depends(get_admin_user)):
|
||||
"""获取文档内容信息(chunks和lines)"""
|
||||
logger.debug(f"GET document {doc_id} content in {db_id}")
|
||||
|
||||
try:
|
||||
info = await knowledge_base.get_file_content(db_id, doc_id)
|
||||
return info
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get file content, {e}, {db_id=}, {doc_id=}, {traceback.format_exc()}")
|
||||
return {"message": "Failed to get file content", "status": "failed"}
|
||||
|
||||
|
||||
@knowledge.delete("/databases/{db_id}/documents/{doc_id}")
|
||||
async def delete_document(db_id: str, doc_id: str, current_user: User = Depends(get_admin_user)):
|
||||
"""删除文档"""
|
||||
@ -183,6 +211,96 @@ async def delete_document(db_id: str, doc_id: str, current_user: User = Depends(
|
||||
raise HTTPException(status_code=400, detail=f"删除文档失败: {e}")
|
||||
|
||||
|
||||
@knowledge.get("/databases/{db_id}/documents/{doc_id}/download")
|
||||
async def download_document(db_id: str, doc_id: str, request: Request, current_user: User = Depends(get_admin_user)):
|
||||
"""下载原始文件"""
|
||||
logger.debug(f"Download document {doc_id} from {db_id}")
|
||||
try:
|
||||
file_info = await knowledge_base.get_file_basic_info(db_id, doc_id)
|
||||
if not file_info:
|
||||
raise HTTPException(status_code=404, detail="File not found")
|
||||
|
||||
file_path = file_info.get("meta", {}).get("path")
|
||||
if not file_path or not os.path.exists(file_path):
|
||||
raise HTTPException(status_code=404, detail=f"File not found on disk: {file_info=}")
|
||||
|
||||
# 获取文件扩展名和MIME类型,解码URL编码的文件名
|
||||
filename = file_info.get("meta", {}).get("filename", "file")
|
||||
logger.debug(f"Original filename from database: {filename}")
|
||||
|
||||
# 解码URL编码的文件名(如果有的话)
|
||||
try:
|
||||
decoded_filename = unquote(filename, encoding='utf-8')
|
||||
logger.debug(f"Decoded filename: {decoded_filename}")
|
||||
except Exception as e:
|
||||
logger.debug(f"Failed to decode filename {filename}: {e}")
|
||||
decoded_filename = filename # 如果解码失败,使用原文件名
|
||||
|
||||
_, ext = os.path.splitext(decoded_filename)
|
||||
|
||||
media_types = {
|
||||
".pdf": "application/pdf",
|
||||
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
".doc": "application/msword",
|
||||
".txt": "text/plain",
|
||||
".md": "text/markdown",
|
||||
".json": "application/json",
|
||||
".csv": "text/csv",
|
||||
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
".xls": "application/vnd.ms-excel",
|
||||
".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
||||
".ppt": "application/vnd.ms-powerpoint",
|
||||
".jpg": "image/jpeg",
|
||||
".jpeg": "image/jpeg",
|
||||
".png": "image/png",
|
||||
".gif": "image/gif",
|
||||
".bmp": "image/bmp",
|
||||
".svg": "image/svg+xml",
|
||||
".zip": "application/zip",
|
||||
".rar": "application/x-rar-compressed",
|
||||
".7z": "application/x-7z-compressed",
|
||||
".tar": "application/x-tar",
|
||||
".gz": "application/gzip",
|
||||
".html": "text/html",
|
||||
".htm": "text/html",
|
||||
".xml": "text/xml",
|
||||
".css": "text/css",
|
||||
".js": "application/javascript",
|
||||
".py": "text/x-python",
|
||||
".java": "text/x-java-source",
|
||||
".cpp": "text/x-c++src",
|
||||
".c": "text/x-csrc",
|
||||
".h": "text/x-chdr",
|
||||
".hpp": "text/x-c++hdr",
|
||||
}
|
||||
media_type = media_types.get(ext.lower(), "application/octet-stream")
|
||||
|
||||
# 创建自定义FileResponse,避免文件名编码问题
|
||||
response = StarletteFileResponse(
|
||||
path=file_path,
|
||||
media_type=media_type
|
||||
)
|
||||
|
||||
# 正确处理中文文件名的HTTP头部设置
|
||||
# HTTP头部只能包含ASCII字符,所以需要对中文文件名进行编码
|
||||
try:
|
||||
# 尝试使用ASCII编码(适用于英文文件名)
|
||||
decoded_filename.encode('ascii')
|
||||
# 如果成功,直接使用简单格式
|
||||
response.headers["Content-Disposition"] = f'attachment; filename="{decoded_filename}"'
|
||||
except UnicodeEncodeError:
|
||||
# 如果包含非ASCII字符(如中文),使用RFC 2231格式
|
||||
encoded_filename = quote(decoded_filename.encode('utf-8'))
|
||||
response.headers["Content-Disposition"] = f"attachment; filename*=UTF-8''{encoded_filename}"
|
||||
|
||||
return response
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"下载文件失败: {e}, {traceback.format_exc()}")
|
||||
raise HTTPException(status_code=500, detail=f"下载失败: {e}")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# === 查询分组 ===
|
||||
# =============================================================================
|
||||
@ -383,6 +501,8 @@ async def upload_file(
|
||||
if not file.filename:
|
||||
raise HTTPException(status_code=400, detail="No selected file")
|
||||
|
||||
logger.debug(f"Received upload file with filename: {file.filename}")
|
||||
|
||||
# 根据db_id获取上传路径,如果db_id为None则使用默认路径
|
||||
if db_id:
|
||||
upload_dir = knowledge_base.get_db_upload_path(db_id)
|
||||
|
||||
@ -285,12 +285,20 @@ class ChromaKB(KnowledgeBase):
|
||||
del self.files_meta[file_id]
|
||||
self._save_metadata()
|
||||
|
||||
async def get_file_info(self, db_id: str, file_id: str) -> dict:
|
||||
"""获取文件信息和chunks"""
|
||||
async def get_file_basic_info(self, db_id: str, file_id: str) -> dict:
|
||||
"""获取文件基本信息(仅元数据)"""
|
||||
if file_id not in self.files_meta:
|
||||
raise Exception(f"File not found: {file_id}")
|
||||
|
||||
return {"meta": self.files_meta[file_id]}
|
||||
|
||||
async def get_file_content(self, db_id: str, file_id: str) -> dict:
|
||||
"""获取文件内容信息(chunks和lines)"""
|
||||
if file_id not in self.files_meta:
|
||||
raise Exception(f"File not found: {file_id}")
|
||||
|
||||
# 使用 ChromaDB 获取chunks
|
||||
content_info = {"lines": []}
|
||||
collection = await self._get_chroma_collection(db_id)
|
||||
if collection:
|
||||
try:
|
||||
@ -313,9 +321,23 @@ class ChromaKB(KnowledgeBase):
|
||||
|
||||
# 按 chunk_order_index 排序
|
||||
doc_chunks.sort(key=lambda x: x.get("chunk_order_index", 0))
|
||||
return {"lines": doc_chunks}
|
||||
content_info["lines"] = doc_chunks
|
||||
return content_info
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting chunks for file {file_id}: {e}")
|
||||
logger.error(f"Failed to get file content from ChromaDB: {e}")
|
||||
content_info["lines"] = []
|
||||
return content_info
|
||||
|
||||
return {"lines": []}
|
||||
return content_info
|
||||
|
||||
async def get_file_info(self, db_id: str, file_id: str) -> dict:
|
||||
"""获取文件完整信息(基本信息+内容信息)- 保持向后兼容"""
|
||||
if file_id not in self.files_meta:
|
||||
raise Exception(f"File not found: {file_id}")
|
||||
|
||||
# 合并基本信息和内容信息
|
||||
basic_info = await self.get_file_basic_info(db_id, file_id)
|
||||
content_info = await self.get_file_content(db_id, file_id)
|
||||
|
||||
return {**basic_info, **content_info}
|
||||
|
||||
@ -230,8 +230,18 @@ class KnowledgeBaseManager:
|
||||
kb_instance = self._get_kb_for_database(db_id)
|
||||
await kb_instance.delete_file(db_id, file_id)
|
||||
|
||||
async def get_file_basic_info(self, db_id: str, file_id: str) -> dict:
|
||||
"""获取文件基本信息(仅元数据)"""
|
||||
kb_instance = self._get_kb_for_database(db_id)
|
||||
return await kb_instance.get_file_basic_info(db_id, file_id)
|
||||
|
||||
async def get_file_content(self, db_id: str, file_id: str) -> dict:
|
||||
"""获取文件内容信息(chunks和lines)"""
|
||||
kb_instance = self._get_kb_for_database(db_id)
|
||||
return await kb_instance.get_file_content(db_id, file_id)
|
||||
|
||||
async def get_file_info(self, db_id: str, file_id: str) -> dict:
|
||||
"""获取文件信息"""
|
||||
"""获取文件完整信息(基本信息+内容信息)- 保持向后兼容"""
|
||||
kb_instance = self._get_kb_for_database(db_id)
|
||||
return await kb_instance.get_file_info(db_id, file_id)
|
||||
|
||||
|
||||
@ -375,16 +375,44 @@ class KnowledgeBase(ABC):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def get_file_info(self, db_id: str, file_id: str) -> dict:
|
||||
async def get_file_basic_info(self, db_id: str, file_id: str) -> dict:
|
||||
"""
|
||||
获取文件信息和chunks
|
||||
获取文件基本信息(仅元数据)
|
||||
|
||||
Args:
|
||||
db_id: 数据库ID
|
||||
file_id: 文件ID
|
||||
|
||||
Returns:
|
||||
文件信息和chunks
|
||||
dict: 包含文件基本信息的字典
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def get_file_content(self, db_id: str, file_id: str) -> dict:
|
||||
"""
|
||||
获取文件内容信息(chunks和lines)
|
||||
|
||||
Args:
|
||||
db_id: 数据库ID
|
||||
file_id: 文件ID
|
||||
|
||||
Returns:
|
||||
dict: 包含文件内容信息的字典
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def get_file_info(self, db_id: str, file_id: str) -> dict:
|
||||
"""
|
||||
获取文件完整信息(基本信息+内容信息)- 保持向后兼容
|
||||
|
||||
Args:
|
||||
db_id: 数据库ID
|
||||
file_id: 文件ID
|
||||
|
||||
Returns:
|
||||
dict: 包含文件信息和chunks的字典
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
@ -310,12 +310,20 @@ class LightRagKB(KnowledgeBase):
|
||||
del self.files_meta[file_id]
|
||||
self._save_metadata()
|
||||
|
||||
async def get_file_info(self, db_id: str, file_id: str) -> dict:
|
||||
"""获取文件信息和chunks"""
|
||||
async def get_file_basic_info(self, db_id: str, file_id: str) -> dict:
|
||||
"""获取文件基本信息(仅元数据)"""
|
||||
if file_id not in self.files_meta:
|
||||
raise Exception(f"File not found: {file_id}")
|
||||
|
||||
return {"meta": self.files_meta[file_id]}
|
||||
|
||||
async def get_file_content(self, db_id: str, file_id: str) -> dict:
|
||||
"""获取文件内容信息(chunks和lines)"""
|
||||
if file_id not in self.files_meta:
|
||||
raise Exception(f"File not found: {file_id}")
|
||||
|
||||
# 使用 LightRAG 获取 chunks
|
||||
content_info = {"lines": []}
|
||||
rag = await self._get_lightrag_instance(db_id)
|
||||
if rag:
|
||||
try:
|
||||
@ -333,12 +341,26 @@ class LightRagKB(KnowledgeBase):
|
||||
|
||||
# 按 chunk_order_index 排序
|
||||
doc_chunks.sort(key=lambda x: x.get("chunk_order_index", 0))
|
||||
return {"lines": doc_chunks}
|
||||
content_info["lines"] = doc_chunks
|
||||
return content_info
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting chunks for file {file_id}: {e}")
|
||||
logger.error(f"Failed to get file content from LightRAG: {e}")
|
||||
content_info["lines"] = []
|
||||
return content_info
|
||||
|
||||
return {"lines": []}
|
||||
return content_info
|
||||
|
||||
async def get_file_info(self, db_id: str, file_id: str) -> dict:
|
||||
"""获取文件完整信息(基本信息+内容信息)- 保持向后兼容"""
|
||||
if file_id not in self.files_meta:
|
||||
raise Exception(f"File not found: {file_id}")
|
||||
|
||||
# 合并基本信息和内容信息
|
||||
basic_info = await self.get_file_basic_info(db_id, file_id)
|
||||
content_info = await self.get_file_content(db_id, file_id)
|
||||
|
||||
return {**basic_info, **content_info}
|
||||
|
||||
async def export_data(self, db_id: str, format: str = "csv", **kwargs) -> str:
|
||||
"""
|
||||
|
||||
@ -377,12 +377,20 @@ class MilvusKB(KnowledgeBase):
|
||||
del self.files_meta[file_id]
|
||||
self._save_metadata()
|
||||
|
||||
async def get_file_info(self, db_id: str, file_id: str) -> dict:
|
||||
"""获取文件信息和chunks"""
|
||||
async def get_file_basic_info(self, db_id: str, file_id: str) -> dict:
|
||||
"""获取文件基本信息(仅元数据)"""
|
||||
if file_id not in self.files_meta:
|
||||
raise Exception(f"File not found: {file_id}")
|
||||
|
||||
return {"meta": self.files_meta[file_id]}
|
||||
|
||||
async def get_file_content(self, db_id: str, file_id: str) -> dict:
|
||||
"""获取文件内容信息(chunks和lines)"""
|
||||
if file_id not in self.files_meta:
|
||||
raise Exception(f"File not found: {file_id}")
|
||||
|
||||
# 使用 Milvus 获取chunks
|
||||
content_info = {"lines": []}
|
||||
collection = await self._get_milvus_collection(db_id)
|
||||
if collection:
|
||||
try:
|
||||
@ -406,12 +414,26 @@ class MilvusKB(KnowledgeBase):
|
||||
|
||||
# 按 chunk_order_index 排序
|
||||
doc_chunks.sort(key=lambda x: x.get("chunk_order_index", 0))
|
||||
return {"lines": doc_chunks}
|
||||
content_info["lines"] = doc_chunks
|
||||
return content_info
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting chunks for file {file_id}: {e}")
|
||||
logger.error(f"Failed to get file content from Milvus: {e}")
|
||||
content_info["lines"] = []
|
||||
return content_info
|
||||
|
||||
return {"lines": []}
|
||||
return content_info
|
||||
|
||||
async def get_file_info(self, db_id: str, file_id: str) -> dict:
|
||||
"""获取文件完整信息(基本信息+内容信息)- 保持向后兼容"""
|
||||
if file_id not in self.files_meta:
|
||||
raise Exception(f"File not found: {file_id}")
|
||||
|
||||
# 合并基本信息和内容信息
|
||||
basic_info = await self.get_file_basic_info(db_id, file_id)
|
||||
content_info = await self.get_file_content(db_id, file_id)
|
||||
|
||||
return {**basic_info, **content_info}
|
||||
|
||||
def delete_database(self, db_id: str) -> dict:
|
||||
"""删除数据库,同时清除Milvus中的集合"""
|
||||
|
||||
@ -11,9 +11,10 @@ import { message } from 'ant-design-vue'
|
||||
* @param {string} url - API端点
|
||||
* @param {Object} options - 请求选项
|
||||
* @param {boolean} requiresAuth - 是否需要认证头
|
||||
* @param {string} responseType - 响应类型: 'json' | 'text' | 'blob'
|
||||
* @returns {Promise} - 请求结果
|
||||
*/
|
||||
export async function apiRequest(url, options = {}, requiresAuth = true) {
|
||||
export async function apiRequest(url, options = {}, requiresAuth = true, responseType = 'json') {
|
||||
try {
|
||||
// 默认请求配置
|
||||
const requestOptions = {
|
||||
@ -43,33 +44,46 @@ export async function apiRequest(url, options = {}, requiresAuth = true) {
|
||||
let errorMessage = `请求失败: ${response.status}, ${response.statusText}`
|
||||
let errorData = null
|
||||
|
||||
console.log('API请求失败:', {
|
||||
url,
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
headers: Object.fromEntries(response.headers.entries())
|
||||
});
|
||||
|
||||
try {
|
||||
errorData = await response.json()
|
||||
errorMessage = errorData.detail || errorData.message || errorMessage
|
||||
console.log('API错误详情:', errorData);
|
||||
} catch (e) {
|
||||
// 如果无法解析JSON,使用默认错误信息
|
||||
console.log('无法解析错误响应JSON:', e);
|
||||
}
|
||||
|
||||
// 特殊处理401和403错误
|
||||
if (response.status === 401) {
|
||||
// 如果是认证失败,可能需要重新登录
|
||||
const userStore = useUserStore()
|
||||
|
||||
// 检查是否是token过期
|
||||
const isTokenExpired = errorData &&
|
||||
(errorData.detail?.includes('令牌已过期') ||
|
||||
errorData.detail?.includes('token expired') ||
|
||||
errorMessage?.includes('令牌已过期') ||
|
||||
errorMessage?.includes('token expired'))
|
||||
|
||||
message.error(isTokenExpired ? '登录已过期,请重新登录' : '认证失败,请重新登录')
|
||||
|
||||
// 如果用户当前认为自己已登录,则登出
|
||||
if (userStore.isLoggedIn) {
|
||||
// 如果用户认为自己已登录,但收到401,则可能是令牌过期
|
||||
const isTokenExpired = errorData &&
|
||||
(errorData.detail?.includes('令牌已过期') ||
|
||||
errorData.detail?.includes('token expired') ||
|
||||
errorMessage?.includes('令牌已过期') ||
|
||||
errorMessage?.includes('token expired'))
|
||||
|
||||
message.error(isTokenExpired ? '登录已过期,请重新登录' : '认证失败,请重新登录')
|
||||
userStore.logout()
|
||||
|
||||
// 使用setTimeout确保消息显示后再跳转
|
||||
setTimeout(() => {
|
||||
window.location.href = '/login'
|
||||
}, 1500)
|
||||
}
|
||||
|
||||
// 使用setTimeout确保消息显示后再跳转
|
||||
setTimeout(() => {
|
||||
window.location.href = '/login'
|
||||
}, 1500)
|
||||
|
||||
throw new Error('未授权,请先登录')
|
||||
} else if (response.status === 403) {
|
||||
throw new Error('没有权限执行此操作')
|
||||
@ -80,13 +94,21 @@ export async function apiRequest(url, options = {}, requiresAuth = true) {
|
||||
throw new Error(errorMessage)
|
||||
}
|
||||
|
||||
// 检查Content-Type以确定如何处理响应
|
||||
const contentType = response.headers.get('Content-Type')
|
||||
if (contentType && contentType.includes('application/json')) {
|
||||
return await response.json()
|
||||
// 根据responseType处理响应
|
||||
if (responseType === 'blob') {
|
||||
return response
|
||||
} else if (responseType === 'json') {
|
||||
// 检查Content-Type以确定如何处理响应
|
||||
const contentType = response.headers.get('Content-Type')
|
||||
if (contentType && contentType.includes('application/json')) {
|
||||
return await response.json()
|
||||
}
|
||||
return await response.text()
|
||||
} else if (responseType === 'text') {
|
||||
return await response.text()
|
||||
} else {
|
||||
return response
|
||||
}
|
||||
|
||||
return await response.text()
|
||||
} catch (error) {
|
||||
console.error('API请求错误:', error)
|
||||
throw error
|
||||
@ -98,20 +120,21 @@ export async function apiRequest(url, options = {}, requiresAuth = true) {
|
||||
* @param {string} url - API端点
|
||||
* @param {Object} options - 请求选项
|
||||
* @param {boolean} requiresAuth - 是否需要认证
|
||||
* @param {string} responseType - 响应类型: 'json' | 'text' | 'blob'
|
||||
* @returns {Promise} - 请求结果
|
||||
*/
|
||||
export function apiGet(url, options = {}, requiresAuth = true) {
|
||||
return apiRequest(url, { method: 'GET', ...options }, requiresAuth)
|
||||
export function apiGet(url, options = {}, requiresAuth = true, responseType = 'json') {
|
||||
return apiRequest(url, { method: 'GET', ...options }, requiresAuth, responseType)
|
||||
}
|
||||
|
||||
export function apiAdminGet(url, options = {}) {
|
||||
export function apiAdminGet(url, options = {}, responseType = 'json') {
|
||||
checkAdminPermission()
|
||||
return apiGet(url, options, true)
|
||||
return apiGet(url, options, true, responseType)
|
||||
}
|
||||
|
||||
export function apiSuperAdminGet(url, options = {}) {
|
||||
export function apiSuperAdminGet(url, options = {}, responseType = 'json') {
|
||||
checkSuperAdminPermission()
|
||||
return apiGet(url, options, true)
|
||||
return apiGet(url, options, true, responseType)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -120,9 +143,10 @@ export function apiSuperAdminGet(url, options = {}) {
|
||||
* @param {Object} data - 请求体数据
|
||||
* @param {Object} options - 其他请求选项
|
||||
* @param {boolean} requiresAuth - 是否需要认证
|
||||
* @param {string} responseType - 响应类型: 'json' | 'text' | 'blob'
|
||||
* @returns {Promise} - 请求结果
|
||||
*/
|
||||
export function apiPost(url, data = {}, options = {}, requiresAuth = true) {
|
||||
export function apiPost(url, data = {}, options = {}, requiresAuth = true, responseType = 'json') {
|
||||
return apiRequest(
|
||||
url,
|
||||
{
|
||||
@ -130,18 +154,19 @@ export function apiPost(url, data = {}, options = {}, requiresAuth = true) {
|
||||
body: JSON.stringify(data),
|
||||
...options
|
||||
},
|
||||
requiresAuth
|
||||
requiresAuth,
|
||||
responseType
|
||||
)
|
||||
}
|
||||
|
||||
export function apiAdminPost(url, data = {}, options = {}) {
|
||||
export function apiAdminPost(url, data = {}, options = {}, responseType = 'json') {
|
||||
checkAdminPermission()
|
||||
return apiPost(url, data, options, true)
|
||||
return apiPost(url, data, options, true, responseType)
|
||||
}
|
||||
|
||||
export function apiSuperAdminPost(url, data = {}, options = {}) {
|
||||
export function apiSuperAdminPost(url, data = {}, options = {}, responseType = 'json') {
|
||||
checkSuperAdminPermission()
|
||||
return apiPost(url, data, options, true)
|
||||
return apiPost(url, data, options, true, responseType)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -150,9 +175,10 @@ export function apiSuperAdminPost(url, data = {}, options = {}) {
|
||||
* @param {Object} data - 请求体数据
|
||||
* @param {Object} options - 其他请求选项
|
||||
* @param {boolean} requiresAuth - 是否需要认证
|
||||
* @param {string} responseType - 响应类型: 'json' | 'text' | 'blob'
|
||||
* @returns {Promise} - 请求结果
|
||||
*/
|
||||
export function apiPut(url, data = {}, options = {}, requiresAuth = true) {
|
||||
export function apiPut(url, data = {}, options = {}, requiresAuth = true, responseType = 'json') {
|
||||
return apiRequest(
|
||||
url,
|
||||
{
|
||||
@ -160,18 +186,19 @@ export function apiPut(url, data = {}, options = {}, requiresAuth = true) {
|
||||
body: JSON.stringify(data),
|
||||
...options
|
||||
},
|
||||
requiresAuth
|
||||
requiresAuth,
|
||||
responseType
|
||||
)
|
||||
}
|
||||
|
||||
export function apiAdminPut(url, data = {}, options = {}) {
|
||||
export function apiAdminPut(url, data = {}, options = {}, responseType = 'json') {
|
||||
checkAdminPermission()
|
||||
return apiPut(url, data, options, true)
|
||||
return apiPut(url, data, options, true, responseType)
|
||||
}
|
||||
|
||||
export function apiSuperAdminPut(url, data = {}, options = {}) {
|
||||
export function apiSuperAdminPut(url, data = {}, options = {}, responseType = 'json') {
|
||||
checkSuperAdminPermission()
|
||||
return apiPut(url, data, options, true)
|
||||
return apiPut(url, data, options, true, responseType)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -179,17 +206,20 @@ export function apiSuperAdminPut(url, data = {}, options = {}) {
|
||||
* @param {string} url - API端点
|
||||
* @param {Object} options - 请求选项
|
||||
* @param {boolean} requiresAuth - 是否需要认证
|
||||
* @param {string} responseType - 响应类型: 'json' | 'text' | 'blob'
|
||||
* @returns {Promise} - 请求结果
|
||||
*/
|
||||
export function apiDelete(url, options = {}, requiresAuth = true) {
|
||||
return apiRequest(url, { method: 'DELETE', ...options }, requiresAuth)
|
||||
export function apiDelete(url, options = {}, requiresAuth = true, responseType = 'json') {
|
||||
return apiRequest(url, { method: 'DELETE', ...options }, requiresAuth, responseType)
|
||||
}
|
||||
|
||||
|
||||
export function apiAdminDelete(url, options = {}) {
|
||||
checkAdminPermission()
|
||||
return apiDelete(url, options, true)
|
||||
}
|
||||
|
||||
|
||||
export function apiSuperAdminDelete(url, options = {}) {
|
||||
checkSuperAdminPermission()
|
||||
return apiDelete(url, options, true)
|
||||
|
||||
@ -93,6 +93,16 @@ export const documentApi = {
|
||||
*/
|
||||
deleteDocument: async (dbId, docId) => {
|
||||
return apiAdminDelete(`/api/knowledge/databases/${dbId}/documents/${docId}`)
|
||||
},
|
||||
|
||||
/**
|
||||
* 下载文档
|
||||
* @param {string} dbId - 知识库ID
|
||||
* @param {string} docId - 文档ID
|
||||
* @returns {Promise} - Response对象
|
||||
*/
|
||||
downloadDocument: async (dbId, docId) => {
|
||||
return apiAdminGet(`/api/knowledge/databases/${dbId}/documents/${docId}/download`, {}, 'blob')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -222,7 +222,7 @@ const currentAgentId = computed(() => {
|
||||
});
|
||||
|
||||
const currentAgentMetadata = computed(() => {
|
||||
if (agentStore?.metadata && currentAgentId.value in agentStore?.metadata[currentAgentId.value]) {
|
||||
if (agentStore?.metadata && currentAgentId.value && currentAgentId.value in agentStore?.metadata[currentAgentId.value]) {
|
||||
return agentStore?.metadata[currentAgentId.value]
|
||||
}
|
||||
return {}
|
||||
|
||||
@ -102,6 +102,12 @@
|
||||
</a-tooltip>
|
||||
|
||||
<div v-else-if="column.key === 'action'" style="display: flex; gap: 4px;">
|
||||
<a-button class="download-btn" type="text"
|
||||
@click="handleDownloadFile(record)"
|
||||
:disabled="lock || record.status !== 'done'"
|
||||
:icon="h(DownloadOutlined)"
|
||||
title="下载"
|
||||
/>
|
||||
<a-button class="del-btn" type="text"
|
||||
@click="handleDeleteFile(record.file_id)"
|
||||
:disabled="lock || record.status === 'processing' || record.status === 'waiting'"
|
||||
@ -118,7 +124,9 @@
|
||||
<script setup>
|
||||
import { ref, computed, watch, h } from 'vue';
|
||||
import { useDatabaseStore } from '@/stores/database';
|
||||
import { Modal } from 'ant-design-vue';
|
||||
import { Modal, message } from 'ant-design-vue';
|
||||
import { useUserStore } from '@/stores/user';
|
||||
import { documentApi } from '@/apis/knowledge_api';
|
||||
import {
|
||||
CheckCircleFilled,
|
||||
HourglassFilled,
|
||||
@ -127,10 +135,12 @@ import {
|
||||
DeleteOutlined,
|
||||
PlusOutlined,
|
||||
ReloadOutlined,
|
||||
DownloadOutlined,
|
||||
} from '@ant-design/icons-vue';
|
||||
import { ChevronLast } from 'lucide-vue-next';
|
||||
|
||||
const store = useDatabaseStore();
|
||||
const userStore = useUserStore();
|
||||
|
||||
const props = defineProps({
|
||||
rightPanelVisible: {
|
||||
@ -189,7 +199,7 @@ const columnsCompact = [
|
||||
},
|
||||
sortDirections: ['ascend', 'descend']
|
||||
},
|
||||
{ title: '', key: 'action', dataIndex: 'file_id', width: 40, align: 'center' }
|
||||
{ title: '', key: 'action', dataIndex: 'file_id', width: 80, align: 'center' }
|
||||
];
|
||||
|
||||
// 过滤后的文件列表
|
||||
@ -284,9 +294,69 @@ const selectAllFailedFiles = () => {
|
||||
};
|
||||
|
||||
const openFileDetail = (record) => {
|
||||
console.log('openFileDetail', record);
|
||||
store.openFileDetail(record);
|
||||
};
|
||||
|
||||
const handleDownloadFile = async (record) => {
|
||||
const dbId = store.databaseId;
|
||||
if (!dbId) {
|
||||
console.error('无法获取数据库ID,数据库ID:', store.databaseId, '记录:', record);
|
||||
message.error('无法获取数据库ID,请刷新页面后重试');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('开始下载文件:', { dbId, fileId: record.file_id, record });
|
||||
|
||||
try {
|
||||
const response = await documentApi.downloadDocument(dbId, record.file_id);
|
||||
|
||||
// 获取文件名
|
||||
const contentDisposition = response.headers.get('content-disposition');
|
||||
let filename = record.filename;
|
||||
if (contentDisposition) {
|
||||
// 首先尝试匹配RFC 2231格式 filename*=UTF-8''...
|
||||
const rfc2231Match = contentDisposition.match(/filename\*=UTF-8''([^;]+)/);
|
||||
if (rfc2231Match) {
|
||||
try {
|
||||
filename = decodeURIComponent(rfc2231Match[1]);
|
||||
} catch (error) {
|
||||
console.warn('Failed to decode RFC2231 filename:', rfc2231Match[1], error);
|
||||
}
|
||||
} else {
|
||||
// 回退到标准格式 filename="..."
|
||||
const filenameMatch = contentDisposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/);
|
||||
if (filenameMatch && filenameMatch[1]) {
|
||||
filename = filenameMatch[1].replace(/['"]/g, '');
|
||||
// 解码URL编码的文件名
|
||||
try {
|
||||
filename = decodeURIComponent(filename);
|
||||
} catch (error) {
|
||||
console.warn('Failed to decode filename:', filename, error);
|
||||
// 如果解码失败,使用原文件名
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 创建blob并下载
|
||||
const blob = await response.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = filename;
|
||||
link.style.display = 'none';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
window.URL.revokeObjectURL(url);
|
||||
} catch (error) {
|
||||
console.error('下载文件时出错:', error);
|
||||
const errorMessage = error.message || '下载失败,请稍后重试';
|
||||
message.error(errorMessage);
|
||||
}
|
||||
};
|
||||
|
||||
// 导入工具函数
|
||||
import { getFileIcon, getFileIconColor, formatRelativeTime } from '@/utils/file_utils';
|
||||
</script>
|
||||
@ -388,6 +458,14 @@ import { getFileIcon, getFileIconColor, formatRelativeTime } from '@/utils/file_
|
||||
color: var(--gray-500);
|
||||
}
|
||||
|
||||
.my-table-compact .download-btn {
|
||||
color: var(--gray-500);
|
||||
}
|
||||
|
||||
.my-table-compact .download-btn:hover {
|
||||
color: var(--main-color);
|
||||
}
|
||||
|
||||
.my-table-compact .del-btn:hover {
|
||||
color: var(--error-color);
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user