feat(知识库): 修改文件管理查询、插入逻辑,新增知识库管理功能 (#25)

知识库管理功能,包括:
1. 新增知识库相关路由、服务和前端接口
2. 新增知识库文档管理功能
3. 新增知识库图标及类型定义
4. 优化文件上传和下载逻辑
5. 新增标准响应格式工具函数
This commit is contained in:
zstar 2025-04-14 10:11:02 +08:00 committed by GitHub
parent 82f468c3df
commit 16b8ca49b5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 2470 additions and 504 deletions

View File

@ -12,7 +12,13 @@ load_dotenv(os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file_
app = Flask(__name__)
# 启用CORS允许前端访问
CORS(app, resources={r"/api/*": {"origins": "*"}}, supports_credentials=True)
CORS(app, resources={
r"/api/*": {
"origins": "*",
"methods": ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
"allow_headers": ["Content-Type", "Authorization"]
}
})
# 注册所有路由
register_routes(app)

View File

@ -6,12 +6,14 @@ users_bp = Blueprint('users', __name__, url_prefix='/api/v1/users')
teams_bp = Blueprint('teams', __name__, url_prefix='/api/v1/teams')
tenants_bp = Blueprint('tenants', __name__, url_prefix='/api/v1/tenants')
files_bp = Blueprint('files', __name__, url_prefix='/api/v1/files')
knowledgebase_bp = Blueprint('knowledgebases', __name__, url_prefix='/api/v1/knowledgebases')
# 导入路由
from .users.routes import *
from .teams.routes import *
from .tenants.routes import *
from .files.routes import *
from .knowledgebases.routes import *
def register_routes(app):
@ -19,4 +21,5 @@ def register_routes(app):
app.register_blueprint(users_bp)
app.register_blueprint(teams_bp)
app.register_blueprint(tenants_bp)
app.register_blueprint(files_bp)
app.register_blueprint(files_bp)
app.register_blueprint(knowledgebase_bp)

View File

@ -15,6 +15,7 @@ from services.files.service import (
get_minio_client,
upload_files_to_server
)
from services.files.utils import FileType
UPLOAD_FOLDER = '/data/uploads'
ALLOWED_EXTENSIONS = {'txt', 'pdf', 'png', 'jpg', 'jpeg', 'gif', 'doc', 'docx', 'xls', 'xlsx'}
@ -26,13 +27,17 @@ def allowed_file(filename):
@files_bp.route('/upload', methods=['POST'])
def upload_file():
if 'files' not in request.files:
return jsonify({'code': 400, 'message': '未选择文件'}), 400
return jsonify({'code': 400, 'message': '未选择文件', 'data': None}), 400
files = request.files.getlist('files')
upload_result = upload_files_to_server(files)
return jsonify(upload_result)
# 返回标准格式
return jsonify({
'code': 0,
'message': '上传成功',
'data': upload_result['data']
})
@files_bp.route('', methods=['GET', 'OPTIONS'])
def get_files():
@ -66,82 +71,56 @@ def get_files():
def download_file(file_id):
try:
current_app.logger.info(f"开始处理文件下载请求: {file_id}")
document, _, storage_bucket, storage_location = get_file_info(file_id)
if not document:
# 获取文件信息
file = get_file_info(file_id)
if not file:
current_app.logger.error(f"文件不存在: {file_id}")
return jsonify({
"code": 404,
"message": f"文件 {file_id} 不存在",
"details": "文件记录不存在或已被删除"
}), 404
if file['type'] == FileType.FOLDER.value:
current_app.logger.error(f"不能下载文件夹: {file_id}")
return jsonify({
"code": 400,
"message": "不能下载文件夹",
"details": "请选择一个文件进行下载"
}), 400
current_app.logger.info(f"文件信息获取成功: {file_id}, 存储位置: {storage_bucket}/{storage_location}")
current_app.logger.info(f"文件信息获取成功: {file_id}, 存储位置: {file['parent_id']}/{file['location']}")
try:
minio_client = get_minio_client()
current_app.logger.info(f"MinIO客户端创建成功, 准备检查文件: {storage_bucket}/{storage_location}")
# 从MinIO下载文件
file_data, filename = download_file_from_minio(file_id)
obj = minio_client.stat_object(storage_bucket, storage_location)
if not obj:
current_app.logger.error(f"文件对象为空: {storage_bucket}/{storage_location}")
return jsonify({
"code": 404,
"message": "文件内容为空",
"details": "MinIO存储桶中存在文件记录但内容为空"
}), 404
if obj.size == 0:
current_app.logger.error(f"文件大小为0: {storage_bucket}/{storage_location}")
return jsonify({
"code": 404,
"message": "文件内容为空",
"details": "MinIO存储桶中文件大小为0"
}), 404
current_app.logger.info(f"文件检查成功, 大小: {obj.size} 字节, 准备下载")
response = minio_client.get_object(storage_bucket, storage_location)
file_data = response.read()
current_app.logger.info(f"文件读取成功, 大小: {len(file_data)} 字节, 准备发送")
# 创建内存文件对象
file_stream = BytesIO(file_data)
# 返回文件
return send_file(
BytesIO(file_data),
mimetype='application/octet-stream',
file_stream,
download_name=filename,
as_attachment=True,
download_name=document['name']
mimetype='application/octet-stream'
)
except Exception as e:
current_app.logger.error(f"MinIO操作异常: {str(e)}", exc_info=True)
# 检查是否是连接错误
if "connection" in str(e).lower():
return jsonify({
"code": 503,
"message": "存储服务连接失败",
"details": f"无法连接到MinIO服务: {str(e)}"
}), 503
# 检查是否是权限错误
elif "access denied" in str(e).lower() or "permission" in str(e).lower():
return jsonify({
"code": 403,
"message": "存储服务访问被拒绝",
"details": f"MinIO访问权限错误: {str(e)}"
}), 403
# 其他错误
else:
return jsonify({
"code": 500,
"message": "存储服务异常",
"details": str(e)
}), 500
current_app.logger.error(f"下载文件失败: {str(e)}")
return jsonify({
"code": 500,
"message": "下载文件失败",
"details": str(e)
}), 500
except Exception as e:
current_app.logger.error(f"文件下载异常: {str(e)}", exc_info=True)
current_app.logger.error(f"处理下载请求时出错: {str(e)}")
return jsonify({
"code": 500,
"message": "文件下载失败",
"message": "处理下载请求时出错",
"details": str(e)
}), 500

View File

@ -0,0 +1,160 @@
from flask import Blueprint, request
from services.knowledgebases.service import KnowledgebaseService
from utils import success_response, error_response
from .. import knowledgebase_bp
@knowledgebase_bp.route('', methods=['GET'])
def get_knowledgebase_list():
"""获取知识库列表"""
try:
params = {
'page': int(request.args.get('currentPage', 1)),
'size': int(request.args.get('size', 10)),
'name': request.args.get('name', '')
}
result = KnowledgebaseService.get_knowledgebase_list(**params)
return success_response(result)
except ValueError as e:
return error_response("参数类型错误", code=400)
except Exception as e:
return error_response(str(e))
@knowledgebase_bp.route('/<string:kb_id>', methods=['GET'])
def get_knowledgebase_detail(kb_id):
"""获取知识库详情"""
try:
knowledgebase = KnowledgebaseService.get_knowledgebase_detail(
kb_id=kb_id
)
if not knowledgebase:
return error_response('知识库不存在', code=404)
return success_response(knowledgebase)
except Exception as e:
return error_response(str(e))
@knowledgebase_bp.route('', methods=['POST'])
def create_knowledgebase():
"""创建知识库"""
try:
data = request.json
if not data.get('name'):
return error_response('知识库名称不能为空', code=400)
# 移除 created_by 参数
kb = KnowledgebaseService.create_knowledgebase(**data)
return success_response(kb, "创建成功", code=0)
except Exception as e:
return error_response(str(e))
@knowledgebase_bp.route('/<string:kb_id>', methods=['PUT'])
def update_knowledgebase(kb_id):
"""更新知识库"""
try:
data = request.json
kb = KnowledgebaseService.update_knowledgebase(
kb_id=kb_id,
**data
)
if not kb:
return error_response('知识库不存在', code=404)
return success_response(kb)
except Exception as e:
return error_response(str(e))
@knowledgebase_bp.route('/<string:kb_id>', methods=['DELETE'])
def delete_knowledgebase(kb_id):
"""删除知识库"""
try:
result = KnowledgebaseService.delete_knowledgebase(
kb_id=kb_id
)
if not result:
return error_response('知识库不存在', code=404)
return success_response(message='删除成功')
except Exception as e:
return error_response(str(e))
@knowledgebase_bp.route('/batch', methods=['DELETE'])
def batch_delete_knowledgebase():
"""批量删除知识库"""
try:
data = request.json
if not data or not data.get('ids'):
return error_response('请选择要删除的知识库', code=400)
result = KnowledgebaseService.batch_delete_knowledgebase(
kb_ids=data['ids']
)
return success_response(message=f'成功删除 {result} 个知识库')
except Exception as e:
return error_response(str(e))
@knowledgebase_bp.route('/<string:kb_id>/documents', methods=['GET'])
def get_knowledgebase_documents(kb_id):
"""获取知识库下的文档列表"""
try:
params = {
'kb_id': kb_id,
'page': int(request.args.get('currentPage', 1)),
'size': int(request.args.get('size', 10)),
'name': request.args.get('name', '')
}
result = KnowledgebaseService.get_knowledgebase_documents(**params)
return success_response(result)
except ValueError as e:
return error_response("参数类型错误", code=400)
except Exception as e:
return error_response(str(e))
@knowledgebase_bp.route('/<string:kb_id>/documents', methods=['POST'])
def add_documents_to_knowledgebase(kb_id):
"""添加文档到知识库"""
try:
print(f"[DEBUG] 接收到添加文档请求kb_id: {kb_id}")
data = request.json
if not data:
print("[ERROR] 请求数据为空")
return error_response('请求数据不能为空', code=400)
file_ids = data.get('file_ids', [])
print(f"[DEBUG] 接收到的file_ids: {file_ids}, 类型: {type(file_ids)}")
try:
result = KnowledgebaseService.add_documents_to_knowledgebase(
kb_id=kb_id,
file_ids=file_ids
)
print(f"[DEBUG] 服务层处理成功,结果: {result}")
return success_response(
data=result,
message="添加成功",
code=201
)
except Exception as service_error:
print(f"[ERROR] 服务层错误详情: {str(service_error)}")
import traceback
traceback.print_exc()
return error_response(str(service_error), code=500)
except Exception as e:
print(f"[ERROR] 路由层错误详情: {str(e)}")
import traceback
traceback.print_exc()
return error_response(str(e), code=500)
@knowledgebase_bp.route('/documents/<string:doc_id>', methods=['DELETE', 'OPTIONS'])
def delete_document(doc_id):
"""删除文档"""
# 处理 OPTIONS 预检请求
if request.method == 'OPTIONS':
response = success_response({})
# 添加 CORS 相关头
response.headers.add('Access-Control-Allow-Methods', 'DELETE')
response.headers.add('Access-Control-Allow-Headers', 'Content-Type,Authorization')
return response
try:
KnowledgebaseService.delete_document(doc_id)
return success_response(message="删除成功")
except Exception as e:
return error_response(str(e))

View File

@ -37,7 +37,7 @@ def filename_type(filename):
return FileType.EXCEL.value
elif ext in ['.ppt', '.pptx']:
return FileType.PPT.value
elif ext in ['.txt', '.md']: # 添加对 txt 和 md 文件的支持
elif ext in ['.txt', '.md']:
return FileType.TEXT.value
return FileType.OTHER.value
@ -55,13 +55,14 @@ def get_db_connection():
"""创建数据库连接"""
return mysql.connector.connect(**DB_CONFIG)
def get_files_list(current_page, page_size, name_filter=""):
def get_files_list(current_page, page_size, parent_id=None, name_filter=""):
"""
获取文件列表
Args:
current_page: 当前页码
page_size: 每页大小
parent_id: 父文件夹ID
name_filter: 文件名过滤条件
Returns:
@ -76,17 +77,21 @@ def get_files_list(current_page, page_size, name_filter=""):
cursor = conn.cursor(dictionary=True)
# 构建查询条件
where_clause = ""
where_clause = "WHERE f.type != 'folder'" # 排除文件夹类型
params = []
if parent_id:
where_clause += " AND f.parent_id = %s"
params.append(parent_id)
if name_filter:
where_clause = "WHERE d.name LIKE %s"
where_clause += " AND f.name LIKE %s"
params.append(f"%{name_filter}%")
# 查询总数
count_query = f"""
SELECT COUNT(*) as total
FROM document d
FROM file f
{where_clause}
"""
cursor.execute(count_query, params)
@ -94,70 +99,19 @@ def get_files_list(current_page, page_size, name_filter=""):
# 查询文件列表
query = f"""
SELECT d.id, d.name, d.kb_id, d.location, d.size, d.type, d.create_time
FROM document d
SELECT f.id, f.name, f.parent_id, f.type, f.size, f.location, f.source_type, f.create_time
FROM file f
{where_clause}
ORDER BY d.create_time DESC
ORDER BY f.create_time DESC
LIMIT %s OFFSET %s
"""
cursor.execute(query, params + [page_size, offset])
documents = cursor.fetchall()
# 获取文档与文件的关联信息
doc_ids = [doc['id'] for doc in documents]
file_mappings = {}
if doc_ids:
placeholders = ', '.join(['%s'] * len(doc_ids))
cursor.execute(f"""
SELECT f2d.document_id, f.id as file_id, f.parent_id, f.source_type
FROM file2document f2d
JOIN file f ON f2d.file_id = f.id
WHERE f2d.document_id IN ({placeholders})
""", doc_ids)
for row in cursor.fetchall():
file_mappings[row['document_id']] = {
'file_id': row['file_id'],
'parent_id': row['parent_id'],
'source_type': row['source_type']
}
# 整合信息
result = []
for doc in documents:
doc_id = doc['id']
kb_id = doc['kb_id']
location = doc['location']
# 确定存储位置
storage_bucket = kb_id
storage_location = location
# 如果有文件映射检查是否需要使用文件的parent_id作为bucket
if doc_ids and doc_id in file_mappings:
file_info = file_mappings[doc_id]
# 模拟File2DocumentService.get_storage_address的逻辑
if file_info.get('source_type') is None or file_info.get('source_type') == 0: # LOCAL
storage_bucket = file_info['parent_id']
# 构建结果字典
result_item = {
'id': doc_id,
'name': doc.get('name', ''),
'kb_id': kb_id,
'size': doc.get('size', 0),
'type': doc.get('type', ''),
'location': location,
'create_time': doc.get('create_time', 0)
}
result.append(result_item)
files = cursor.fetchall()
cursor.close()
conn.close()
return result, total
return files, total
except Exception as e:
raise e
@ -170,69 +124,53 @@ def get_file_info(file_id):
file_id: 文件ID
Returns:
tuple: (文档信息, 文件映射信息, 存储桶, 存储位置)
dict: 文件信息
"""
try:
# 连接数据库
conn = get_db_connection()
cursor = conn.cursor(dictionary=True)
# 查询文信息
# 查询文信息
cursor.execute("""
SELECT d.id, d.name, d.kb_id, d.location, d.type
FROM document d
WHERE d.id = %s
SELECT id, name, parent_id, type, size, location, source_type
FROM file
WHERE id = %s
""", (file_id,))
document = cursor.fetchone()
if not document:
cursor.close()
conn.close()
return None, None, None, None
# 获取文档与文件的关联信息
cursor.execute("""
SELECT f2d.document_id, f.id as file_id, f.parent_id, f.source_type
FROM file2document f2d
JOIN file f ON f2d.file_id = f.id
WHERE f2d.document_id = %s
""", (file_id,))
file_mapping = cursor.fetchone()
# 确定存储位置
storage_bucket = document['kb_id']
storage_location = document['location']
# 如果有文件映射检查是否需要使用文件的parent_id作为bucket
if file_mapping:
# 模拟File2DocumentService.get_storage_address的逻辑
if file_mapping.get('source_type') is None or file_mapping.get('source_type') == 0: # LOCAL
storage_bucket = file_mapping['parent_id']
file = cursor.fetchone()
cursor.close()
conn.close()
return document, file_mapping, storage_bucket, storage_location
return file
except Exception as e:
raise e
def download_file_from_minio(storage_bucket, storage_location):
def download_file_from_minio(file_id):
"""
从MinIO下载文件
Args:
storage_bucket: 存储桶
storage_location: 存储位置
file_id: 文件ID
Returns:
bytes: 文件数据
tuple: (文件数据, 文件名)
"""
try:
# 获取文件信息
file = get_file_info(file_id)
if not file:
raise Exception(f"文件 {file_id} 不存在")
# 从MinIO下载文件
minio_client = get_minio_client()
# 使用parent_id作为存储桶
storage_bucket = file['parent_id']
storage_location = file['location']
# 检查bucket是否存在
if not minio_client.bucket_exists(storage_bucket):
raise Exception(f"存储桶 {storage_bucket} 不存在")
@ -241,7 +179,7 @@ def download_file_from_minio(storage_bucket, storage_location):
response = minio_client.get_object(storage_bucket, storage_location)
file_data = response.read()
return file_data
return file_data, file['name']
except Exception as e:
raise e
@ -257,56 +195,98 @@ def delete_file(file_id):
bool: 是否删除成功
"""
try:
# 获取文件信息
document, file_mapping, storage_bucket, storage_location = get_file_info(file_id)
if not document:
return False
# 连接数据库
conn = get_db_connection()
cursor = conn.cursor(dictionary=True)
# 如果有文件映射获取文件ID
file_id_to_delete = None
if file_mapping:
file_id_to_delete = file_mapping['file_id']
# 查询文件信息
cursor.execute("""
SELECT id, parent_id, name, location, type
FROM file
WHERE id = %s
""", (file_id,))
file = cursor.fetchone()
if not file:
cursor.close()
conn.close()
return False
# 如果是文件夹,直接返回成功(不处理文件夹)
if file['type'] == FileType.FOLDER.value:
cursor.close()
conn.close()
return True
# 查询关联的document记录
cursor.execute("""
SELECT f2d.document_id, d.kb_id, d.location
FROM file2document f2d
JOIN document d ON f2d.document_id = d.id
WHERE f2d.file_id = %s
""", (file_id,))
document_mappings = cursor.fetchall()
# 创建MinIO客户端在事务外创建
minio_client = get_minio_client()
# 开始事务
conn.start_transaction()
try:
# 1. 删除document表中的记录
cursor.execute("DELETE FROM document WHERE id = %s", (file_id,))
# 注意这里不再使用conn.start_transaction()而是使用execute直接执行事务相关命令
cursor.execute("START TRANSACTION")
# 2. 如果有关联的file2document记录删除它
if file_mapping:
cursor.execute("DELETE FROM file2document WHERE document_id = %s", (file_id,))
# 1. 先删除file表中的记录
cursor.execute("DELETE FROM file WHERE id = %s", (file_id,))
# 3. 如果有关联的file记录删除它
if file_id_to_delete:
cursor.execute("DELETE FROM file WHERE id = %s", (file_id_to_delete,))
# 2. 删除关联的file2document记录
cursor.execute("DELETE FROM file2document WHERE file_id = %s", (file_id,))
# 3. 删除关联的document记录
for doc_mapping in document_mappings:
cursor.execute("DELETE FROM document WHERE id = %s", (doc_mapping['document_id'],))
# 提交事务
conn.commit()
cursor.execute("COMMIT")
# 从MinIO删除文件
# 从MinIO删除文件(在事务提交后进行)
try:
minio_client = get_minio_client()
# 检查bucket是否存在如果不存在则跳过MinIO删除操作
parent_id = file.get('parent_id')
if parent_id and minio_client.bucket_exists(parent_id):
try:
# 删除文件,忽略文件不存在的错误
minio_client.remove_object(parent_id, file['location'])
print(f"从MinIO删除文件成功: {parent_id}/{file['location']}")
except Exception as e:
print(f"从MinIO删除文件失败: {parent_id}/{file['location']} - {str(e)}")
else:
print(f"存储桶不存在跳过MinIO删除操作: {parent_id}")
# 检查bucket是否存在
if minio_client.bucket_exists(storage_bucket):
# 删除文件
minio_client.remove_object(storage_bucket, storage_location)
# 如果有关联的document也删除document存储的文件
for doc_mapping in document_mappings:
kb_id = doc_mapping.get('kb_id')
doc_location = doc_mapping.get('location')
if kb_id and doc_location and minio_client.bucket_exists(kb_id):
try:
minio_client.remove_object(kb_id, doc_location)
print(f"从MinIO删除document文件成功: {kb_id}/{doc_location}")
except Exception as e:
print(f"从MinIO删除document文件失败: {kb_id}/{doc_location} - {str(e)}")
else:
print(f"document存储桶不存在或位置为空跳过MinIO删除操作: {kb_id}/{doc_location}")
except Exception as e:
# 即使MinIO删除失败也不影响数据库操作的成功
print(f"从MinIO删除文件失败: {str(e)}")
print(f"MinIO操作失败,但不影响数据库删除: {str(e)}")
return True
except Exception as e:
# 回滚事务
conn.rollback()
try:
cursor.execute("ROLLBACK")
except:
pass
raise e
finally:
@ -314,6 +294,7 @@ def delete_file(file_id):
conn.close()
except Exception as e:
print(f"删除文件时发生错误: {str(e)}")
raise e
def batch_delete_files(file_ids):
@ -338,76 +319,93 @@ def batch_delete_files(file_ids):
minio_client = get_minio_client()
# 开始事务
conn.start_transaction()
try:
cursor.execute("START TRANSACTION")
success_count = 0
for file_id in file_ids:
# 查询文信息
# 查询文信息
cursor.execute("""
SELECT d.id, d.kb_id, d.location
FROM document d
WHERE d.id = %s
SELECT id, parent_id, name, location, type
FROM file
WHERE id = %s
""", (file_id,))
document = cursor.fetchone()
if not document:
file = cursor.fetchone()
if not file:
continue
# 获取文档与文件的关联信息
# 如果是文件夹,跳过
if file['type'] == FileType.FOLDER.value:
continue
# 查询关联的document记录
cursor.execute("""
SELECT f2d.id as f2d_id, f2d.document_id, f2d.file_id, f.parent_id, f.source_type
SELECT f2d.id as f2d_id, f2d.document_id, d.kb_id, d.location
FROM file2document f2d
JOIN file f ON f2d.file_id = f.id
WHERE f2d.document_id = %s
JOIN document d ON f2d.document_id = d.id
WHERE f2d.file_id = %s
""", (file_id,))
file_mapping = cursor.fetchone()
document_mappings = cursor.fetchall()
# 确定存储位置
storage_bucket = document['kb_id']
storage_location = document['location']
# 1. 先删除file表中的记录
cursor.execute("DELETE FROM file WHERE id = %s", (file_id,))
# 如果有文件映射检查是否需要使用文件的parent_id作为bucket
file_id_to_delete = None
if file_mapping:
file_id_to_delete = file_mapping['file_id']
# 模拟File2DocumentService.get_storage_address的逻辑
if file_mapping.get('source_type') is None or file_mapping.get('source_type') == 0: # LOCAL
storage_bucket = file_mapping['parent_id']
# 2. 删除关联的file2document记录
cursor.execute("DELETE FROM file2document WHERE file_id = %s", (file_id,))
# 1. 删除document表中的记录
cursor.execute("DELETE FROM document WHERE id = %s", (file_id,))
# 2. 如果有关联的file2document记录删除它
if file_mapping:
cursor.execute("DELETE FROM file2document WHERE id = %s", (file_mapping['f2d_id'],))
# 3. 如果有关联的file记录删除它
if file_id_to_delete:
cursor.execute("DELETE FROM file WHERE id = %s", (file_id_to_delete,))
# 从MinIO删除文件
try:
# 检查bucket是否存在
if minio_client.bucket_exists(storage_bucket):
# 删除文件
minio_client.remove_object(storage_bucket, storage_location)
except Exception as e:
# 即使MinIO删除失败也不影响数据库操作的成功
print(f"从MinIO删除文件失败: {str(e)}")
# 3. 删除关联的document记录
for doc_mapping in document_mappings:
cursor.execute("DELETE FROM document WHERE id = %s", (doc_mapping['document_id'],))
success_count += 1
# 提交事务
conn.commit()
cursor.execute("COMMIT")
# 从MinIO删除文件在事务提交后进行
for file_id in file_ids:
try:
# 查询文件信息
cursor.execute("""
SELECT id, parent_id, name, location, type
FROM file
WHERE id = %s
""", (file_id,))
file = cursor.fetchone()
if not file and file['type'] != FileType.FOLDER.value:
# 检查bucket是否存在
if minio_client.bucket_exists(file['parent_id']):
# 删除文件
minio_client.remove_object(file['parent_id'], file['location'])
# 如果有关联的document也删除document存储的文件
cursor.execute("""
SELECT f2d.id as f2d_id, f2d.document_id, d.kb_id, d.location
FROM file2document f2d
JOIN document d ON f2d.document_id = d.id
WHERE f2d.file_id = %s
""", (file_id,))
document_mappings = cursor.fetchall()
for doc_mapping in document_mappings:
if minio_client.bucket_exists(doc_mapping['kb_id']):
minio_client.remove_object(doc_mapping['kb_id'], doc_mapping['location'])
except Exception as e:
# 即使MinIO删除失败也不影响数据库操作的成功
print(f"从MinIO删除文件失败: {str(e)}")
return success_count
except Exception as e:
# 回滚事务
conn.rollback()
try:
cursor.execute("ROLLBACK")
except:
pass
raise e
finally:
@ -415,9 +413,10 @@ def batch_delete_files(file_ids):
conn.close()
except Exception as e:
print(f"批量删除文件时发生错误: {str(e)}")
raise e
def upload_files_to_server(files, kb_id=None, user_id=None, parent_id=None):
def upload_files_to_server(files, parent_id=None, user_id=None):
"""处理文件上传到服务器的核心逻辑"""
if user_id is None:
try:
@ -446,57 +445,34 @@ def upload_files_to_server(files, kb_id=None, user_id=None, parent_id=None):
print(f"查询最早用户ID失败: {str(e)}")
user_id = 'system'
# 如果没有指定parent_id则获取用户的根文件夹ID
# 如果没有指定parent_id则获取file表中的第一个记录作为parent_id
if parent_id is None:
try:
conn = get_db_connection()
cursor = conn.cursor(dictionary=True)
# 查询用户的根文件夹
query_root_folder = """
# 查询file表中的第一个记录
query_first_file = """
SELECT id FROM file
WHERE tenant_id = %s AND parent_id = id
LIMIT 1
"""
cursor.execute(query_root_folder, (user_id,))
root_folder = cursor.fetchone()
cursor.execute(query_first_file)
first_file = cursor.fetchone()
if root_folder:
parent_id = root_folder['id']
print(f"使用用户根文件夹ID: {parent_id}")
if first_file:
parent_id = first_file['id']
print(f"使用file表中的第一个记录ID作为parent_id: {parent_id}")
else:
# 如果没有找到根文件夹,创建一个
root_id = get_uuid()
# 修改时间格式,包含时分秒
current_time = int(datetime.now().timestamp())
current_date = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
root_folder = {
"id": root_id,
"parent_id": root_id, # 根文件夹的parent_id指向自己
"tenant_id": user_id,
"created_by": user_id,
"name": "/",
"type": FileType.FOLDER.value,
"size": 0,
"location": "",
"source_type": FileSource.LOCAL.value,
"create_time": current_time,
"create_date": current_date,
"update_time": current_time,
"update_date": current_date
}
FileService.insert(root_folder)
parent_id = root_id
print(f"创建并使用新的根文件夹ID: {parent_id}")
# 如果没有找到记录创建一个新的ID
parent_id = get_uuid()
print(f"file表中没有记录创建新的parent_id: {parent_id}")
cursor.close()
conn.close()
except Exception as e:
print(f"查询根文件夹ID失败: {str(e)}")
# 如果无法获取根文件夹使用file_bucket_id作为备选
parent_id = None
print(f"查询file表第一个记录失败: {str(e)}")
parent_id = get_uuid() # 如果无法获取生成一个新的ID
print(f"生成新的parent_id: {parent_id}")
results = []
@ -505,8 +481,6 @@ def upload_files_to_server(files, kb_id=None, user_id=None, parent_id=None):
continue
if file and allowed_file(file.filename):
# 为每个文件生成独立的存储桶名称
file_bucket_id = FileService.generate_bucket_name()
original_filename = file.filename
# 修复文件名处理逻辑,保留中文字符
name, ext = os.path.splitext(original_filename)
@ -526,9 +500,8 @@ def upload_files_to_server(files, kb_id=None, user_id=None, parent_id=None):
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
file.save(filepath)
print(f"文件已保存到临时目录: {filepath}")
print(f"原始文件名: {original_filename}, 处理后文件名: {filename}, 扩展名: {ext[1:]}") # 修改打印信息
# 2. 获取文件类型 - 使用修复后的文件名
# 2. 获取文件类型
filetype = filename_type(filename)
if filetype == FileType.OTHER.value:
raise RuntimeError("不支持的文件类型")
@ -537,103 +510,59 @@ def upload_files_to_server(files, kb_id=None, user_id=None, parent_id=None):
minio_client = get_minio_client()
location = filename
# 确保bucket存在使用文件独立的bucket
if not minio_client.bucket_exists(file_bucket_id):
minio_client.make_bucket(file_bucket_id)
print(f"创建MinIO存储桶: {file_bucket_id}")
# 确保bucket存在
if not minio_client.bucket_exists(parent_id):
minio_client.make_bucket(parent_id)
print(f"创建MinIO存储桶: {parent_id}")
# 4. 上传到MinIO使用文件独立的bucket
# 4. 上传到MinIO
with open(filepath, 'rb') as file_data:
minio_client.put_object(
bucket_name=file_bucket_id,
bucket_name=parent_id,
object_name=location,
data=file_data,
length=os.path.getsize(filepath)
)
print(f"文件已上传到MinIO: {file_bucket_id}/{location}")
print(f"文件已上传到MinIO: {parent_id}/{location}")
# 5. 创建缩略图(如果是图片/PDF等)
thumbnail_location = ''
if filetype in [FileType.VISUAL.value, FileType.PDF.value]:
try:
thumbnail_location = f'thumbnail_{get_uuid()}.png'
except Exception as e:
print(f"生成缩略图失败: {str(e)}")
# 6. 创建数据库记录
doc_id = get_uuid()
# 修改时间格式,包含时分秒
# 5. 创建文件记录
file_id = get_uuid()
current_time = int(datetime.now().timestamp())
current_date = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
doc = {
"id": doc_id,
"kb_id": file_bucket_id, # 使用文件独立的bucket_id
"parser_id": FileService.get_parser(filetype, filename, ""),
"parser_config": {"pages": [[1, 1000000]]},
"source_type": "local",
"created_by": user_id or 'system',
"type": filetype,
file_record = {
"id": file_id,
"parent_id": parent_id,
"tenant_id": user_id,
"created_by": user_id,
"name": filename,
"location": location,
"type": filetype,
"size": os.path.getsize(filepath),
"thumbnail": thumbnail_location,
"token_num": 0,
"chunk_num": 0,
"progress": 0,
"progress_msg": "",
"run": "0",
"status": StatusEnum.VALID.value,
"location": location,
"source_type": FileSource.LOCAL.value,
"create_time": current_time,
"create_date": current_date,
"update_time": current_time,
"update_date": current_date
}
# 7. 保存文档记录 (添加事务处理)
# 保存文件记录
conn = get_db_connection()
try:
cursor = conn.cursor()
DocumentService.insert(doc)
print(f"文档记录已保存到MySQL: {doc_id}")
# 8. 创建文件记录和关联
file_record = {
"id": get_uuid(),
"parent_id": parent_id or file_bucket_id, # 优先使用指定的parent_id
"tenant_id": user_id or 'system',
"created_by": user_id or 'system',
"name": filename,
"type": filetype,
"size": doc["size"],
"location": location,
"source_type": FileSource.KNOWLEDGEBASE.value,
"create_time": current_time,
"create_date": current_date,
"update_time": current_time,
"update_date": current_date
}
FileService.insert(file_record)
print(f"文件记录已保存到MySQL: {file_record['id']}")
# 9. 创建文件-文档关联
File2DocumentService.insert({
"id": get_uuid(),
"file_id": file_record["id"],
"document_id": doc_id,
"create_time": current_time,
"create_date": current_date,
"update_time": current_time,
"update_date": current_date
})
print(f"关联记录已保存到MySQL: {file_record['id']} -> {doc_id}")
# 插入文件记录
columns = ', '.join(file_record.keys())
placeholders = ', '.join(['%s'] * len(file_record))
query = f"INSERT INTO file ({columns}) VALUES ({placeholders})"
cursor.execute(query, list(file_record.values()))
conn.commit()
results.append({
'id': doc_id,
'id': file_id,
'name': filename,
'size': doc["size"],
'size': file_record["size"],
'type': filetype,
'status': 'success'
})

View File

@ -0,0 +1,661 @@
import mysql.connector
import json
from flask import current_app
from datetime import datetime
from utils import generate_uuid
from database import DB_CONFIG
class KnowledgebaseService:
@classmethod
def _get_db_connection(cls):
"""Get database connection"""
return mysql.connector.connect(**DB_CONFIG)
@classmethod
def get_knowledgebase_list(cls, page=1, size=10, name=''):
"""获取知识库列表"""
conn = cls._get_db_connection()
cursor = conn.cursor(dictionary=True)
query = """
SELECT
k.id,
k.name,
k.description,
k.create_date,
k.update_date,
k.doc_num,
k.language,
k.permission
FROM knowledgebase k
"""
params = []
if name:
query += " WHERE k.name LIKE %s"
params.append(f"%{name}%")
query += " LIMIT %s OFFSET %s"
params.extend([size, (page-1)*size])
cursor.execute(query, params)
results = cursor.fetchall()
# 处理结果
for result in results:
# 处理空描述
if not result.get('description'):
result['description'] = "暂无描述"
# 处理时间格式
if result.get('create_date'):
if isinstance(result['create_date'], datetime):
result['create_date'] = result['create_date'].strftime('%Y-%m-%d %H:%M:%S')
elif isinstance(result['create_date'], str):
try:
# 尝试解析已有字符串格式
datetime.strptime(result['create_date'], '%Y-%m-%d %H:%M:%S')
except ValueError:
result['create_date'] = ""
# 获取总数
count_query = "SELECT COUNT(*) as total FROM knowledgebase"
if name:
count_query += " WHERE name LIKE %s"
cursor.execute(count_query, params[:1] if name else [])
total = cursor.fetchone()['total']
cursor.close()
conn.close()
return {
'list': results,
'total': total
}
@classmethod
def get_knowledgebase_detail(cls, kb_id):
"""获取知识库详情"""
conn = cls._get_db_connection()
cursor = conn.cursor(dictionary=True)
query = """
SELECT
k.id,
k.name,
k.description,
k.create_date,
k.update_date,
k.doc_num
FROM knowledgebase k
WHERE k.id = %s
"""
cursor.execute(query, (kb_id,))
result = cursor.fetchone()
if result:
# 处理空描述
if not result.get('description'):
result['description'] = "暂无描述"
# 处理时间格式
if result.get('create_date'):
if isinstance(result['create_date'], datetime):
result['create_date'] = result['create_date'].strftime('%Y-%m-%d %H:%M:%S')
elif isinstance(result['create_date'], str):
try:
datetime.strptime(result['create_date'], '%Y-%m-%d %H:%M:%S')
except ValueError:
result['create_date'] = ""
cursor.close()
conn.close()
return result
@classmethod
def _check_name_exists(cls, name):
"""检查知识库名称是否已存在"""
conn = cls._get_db_connection()
cursor = conn.cursor()
query = """
SELECT COUNT(*) as count
FROM knowledgebase
WHERE name = %s
"""
cursor.execute(query, (name,))
result = cursor.fetchone()
cursor.close()
conn.close()
return result[0] > 0
@classmethod
def create_knowledgebase(cls, **data):
"""创建知识库"""
try:
# 检查知识库名称是否已存在
exists = cls._check_name_exists(data['name'])
if exists:
raise Exception("知识库名称已存在")
conn = cls._get_db_connection()
cursor = conn.cursor(dictionary=True)
# 获取最早的用户ID作为tenant_id和created_by
tenant_id = None
created_by = None
try:
query_earliest_user = """
SELECT id FROM user
WHERE create_time = (SELECT MIN(create_time) FROM user)
LIMIT 1
"""
cursor.execute(query_earliest_user)
earliest_user = cursor.fetchone()
if earliest_user:
tenant_id = earliest_user['id']
created_by = earliest_user['id'] # 使用最早用户ID作为created_by
print(f"使用创建时间最早的用户ID作为tenant_id和created_by: {tenant_id}")
else:
# 如果找不到用户,使用默认值
tenant_id = "system"
created_by = "system"
print(f"未找到用户, 使用默认值作为tenant_id和created_by: {tenant_id}")
except Exception as e:
print(f"获取用户ID失败: {str(e)},使用默认值")
tenant_id = "system"
created_by = "system"
current_time = datetime.now()
create_date = current_time.strftime('%Y-%m-%d %H:%M:%S')
create_time = int(current_time.timestamp() * 1000) # 毫秒级时间戳
update_date = create_date
update_time = create_time
# 完整的字段列表
query = """
INSERT INTO knowledgebase (
id, create_time, create_date, update_time, update_date,
avatar, tenant_id, name, language, description,
embd_id, permission, created_by, doc_num, token_num,
chunk_num, similarity_threshold, vector_similarity_weight, parser_id, parser_config,
pagerank, status
) VALUES (
%s, %s, %s, %s, %s,
%s, %s, %s, %s, %s,
%s, %s, %s, %s, %s,
%s, %s, %s, %s, %s,
%s, %s
)
"""
# 设置默认值
default_parser_config = json.dumps({
"layout_recognize": "DeepDOC",
"chunk_token_num": 512,
"delimiter": "\n!?;。;!?",
"auto_keywords": 0,
"auto_questions": 0,
"html4excel": False,
"raptor": {"use_raptor": False},
"graphrag": {"use_graphrag": False}
})
kb_id = generate_uuid()
cursor.execute(query, (
kb_id, # id
create_time, # create_time
create_date, # create_date
update_time, # update_time
update_date, # update_date
None, # avatar
tenant_id, # tenant_id
data['name'], # name
data.get('language', 'Chinese'), # language
data.get('description', ''), # description
'bge-m3:latest@Ollama', # embd_id
data.get('permission', 'me'), # permission
created_by, # created_by - 使用内部获取的值
0, # doc_num
0, # token_num
0, # chunk_num
0.7, # similarity_threshold
0.3, # vector_similarity_weight
'naive', # parser_id
default_parser_config, # parser_config
0, # pagerank
'1' # status
))
conn.commit()
cursor.close()
conn.close()
# 返回创建后的知识库详情
return cls.get_knowledgebase_detail(kb_id)
except Exception as e:
current_app.logger.error(f"创建知识库失败: {str(e)}")
raise Exception(f"创建知识库失败: {str(e)}")
@classmethod
def update_knowledgebase(cls, kb_id, **data):
"""更新知识库"""
try:
# 直接通过ID检查知识库是否存在
kb = cls.get_knowledgebase_detail(kb_id)
if not kb:
return None
conn = cls._get_db_connection()
cursor = conn.cursor()
# 如果要更新名称,先检查名称是否已存在
if data.get('name') and data['name'] != kb['name']:
exists = cls._check_name_exists(data['name'])
if exists:
raise Exception("知识库名称已存在")
# 构建更新语句
update_fields = []
params = []
if data.get('name'):
update_fields.append("name = %s")
params.append(data['name'])
if 'description' in data:
update_fields.append("description = %s")
params.append(data['description'])
# 更新时间
current_time = datetime.now()
update_date = current_time.strftime('%Y-%m-%d %H:%M:%S')
update_fields.append("update_date = %s")
params.append(update_date)
# 如果没有要更新的字段,直接返回
if not update_fields:
return kb_id
# 构建并执行更新语句
query = f"""
UPDATE knowledgebase
SET {', '.join(update_fields)}
WHERE id = %s
"""
params.append(kb_id)
cursor.execute(query, params)
conn.commit()
cursor.close()
conn.close()
# 返回更新后的知识库详情
return cls.get_knowledgebase_detail(kb_id)
except Exception as e:
print(f"更新知识库失败: {str(e)}")
raise Exception(f"更新知识库失败: {str(e)}")
@classmethod
def delete_knowledgebase(cls, kb_id):
"""删除知识库"""
try:
conn = cls._get_db_connection()
cursor = conn.cursor()
# 先检查知识库是否存在
check_query = "SELECT id FROM knowledgebase WHERE id = %s"
cursor.execute(check_query, (kb_id,))
if not cursor.fetchone():
raise Exception("知识库不存在")
# 执行删除
delete_query = "DELETE FROM knowledgebase WHERE id = %s"
cursor.execute(delete_query, (kb_id,))
conn.commit()
cursor.close()
conn.close()
return True
except Exception as e:
current_app.logger.error(f"删除知识库失败: {str(e)}")
raise Exception(f"删除知识库失败: {str(e)}")
@classmethod
def batch_delete_knowledgebase(cls, kb_ids):
"""批量删除知识库"""
try:
conn = cls._get_db_connection()
cursor = conn.cursor()
# 检查所有ID是否存在
check_query = "SELECT id FROM knowledgebase WHERE id IN (%s)" % \
','.join(['%s'] * len(kb_ids))
cursor.execute(check_query, kb_ids)
existing_ids = [row[0] for row in cursor.fetchall()]
if len(existing_ids) != len(kb_ids):
missing_ids = set(kb_ids) - set(existing_ids)
raise Exception(f"以下知识库不存在: {', '.join(missing_ids)}")
# 执行批量删除
delete_query = "DELETE FROM knowledgebase WHERE id IN (%s)" % \
','.join(['%s'] * len(kb_ids))
cursor.execute(delete_query, kb_ids)
conn.commit()
cursor.close()
conn.close()
return len(kb_ids)
except Exception as e:
current_app.logger.error(f"批量删除知识库失败: {str(e)}")
raise Exception(f"批量删除知识库失败: {str(e)}")
@classmethod
def get_knowledgebase_documents(cls, kb_id, page=1, size=10, name=''):
"""获取知识库下的文档列表"""
try:
conn = cls._get_db_connection()
cursor = conn.cursor(dictionary=True)
# 先检查知识库是否存在
check_query = "SELECT id FROM knowledgebase WHERE id = %s"
cursor.execute(check_query, (kb_id,))
if not cursor.fetchone():
raise Exception("知识库不存在")
# 查询文档列表
query = """
SELECT
d.id,
d.name,
d.chunk_num,
d.create_date,
d.status,
d.run,
d.progress,
d.parser_id,
d.parser_config,
d.meta_fields
FROM document d
WHERE d.kb_id = %s
"""
params = [kb_id]
if name:
query += " AND d.name LIKE %s"
params.append(f"%{name}%")
query += " ORDER BY d.create_date DESC LIMIT %s OFFSET %s"
params.extend([size, (page-1)*size])
cursor.execute(query, params)
results = cursor.fetchall()
# 处理日期时间格式
for result in results:
if result.get('create_date'):
result['create_date'] = result['create_date'].strftime('%Y-%m-%d %H:%M:%S')
# 获取总数
count_query = "SELECT COUNT(*) as total FROM document WHERE kb_id = %s"
count_params = [kb_id]
if name:
count_query += " AND name LIKE %s"
count_params.append(f"%{name}%")
cursor.execute(count_query, count_params)
total = cursor.fetchone()['total']
cursor.close()
conn.close()
print(results)
return {
'list': results,
'total': total
}
except Exception as e:
current_app.logger.error(f"获取知识库文档列表失败: {str(e)}")
raise Exception(f"获取知识库文档列表失败: {str(e)}")
@classmethod
def add_documents_to_knowledgebase(cls, kb_id, file_ids, created_by=None):
"""添加文档到知识库"""
try:
print(f"[DEBUG] 开始添加文档,参数: kb_id={kb_id}, file_ids={file_ids}")
# 如果没有传入created_by则获取最早的用户ID
if created_by is None:
conn = cls._get_db_connection()
cursor = conn.cursor(dictionary=True)
# 查询创建时间最早的用户ID
query_earliest_user = """
SELECT id FROM user
WHERE create_time = (SELECT MIN(create_time) FROM user)
LIMIT 1
"""
cursor.execute(query_earliest_user)
earliest_user = cursor.fetchone()
if earliest_user:
created_by = earliest_user['id']
print(f"使用创建时间最早的用户ID: {created_by}")
else:
created_by = 'system'
print("未找到用户, 使用默认用户ID: system")
cursor.close()
conn.close()
# 检查知识库是否存在
kb = cls.get_knowledgebase_detail(kb_id)
print(f"[DEBUG] 知识库检查结果: {kb}")
if not kb:
print(f"[ERROR] 知识库不存在: {kb_id}")
raise Exception("知识库不存在")
conn = cls._get_db_connection()
cursor = conn.cursor()
# 获取文件信息
file_query = """
SELECT id, name, location, size, type
FROM file
WHERE id IN (%s)
""" % ','.join(['%s'] * len(file_ids))
print(f"[DEBUG] 执行文件查询SQL: {file_query}")
print(f"[DEBUG] 查询参数: {file_ids}")
try:
cursor.execute(file_query, file_ids)
files = cursor.fetchall()
print(f"[DEBUG] 查询到的文件数据: {files}")
except Exception as e:
print(f"[ERROR] 文件查询失败: {str(e)}")
raise
if len(files) != len(file_ids):
print(f"部分文件不存在: 期望={len(file_ids)}, 实际={len(files)}")
raise Exception("部分文件不存在")
# 添加文档记录
added_count = 0
for file in files:
file_id = file[0]
file_name = file[1]
print(f"处理文件: id={file_id}, name={file_name}")
file_location = file[2]
file_size = file[3]
file_type = file[4]
# 检查文档是否已存在于知识库
check_query = """
SELECT COUNT(*)
FROM document d
JOIN file2document f2d ON d.id = f2d.document_id
WHERE d.kb_id = %s AND f2d.file_id = %s
"""
cursor.execute(check_query, (kb_id, file_id))
exists = cursor.fetchone()[0] > 0
if exists:
continue # 跳过已存在的文档
# 创建文档记录
doc_id = generate_uuid()
current_datetime = datetime.now()
create_time = int(current_datetime.timestamp() * 1000) # 毫秒级时间戳
current_date = current_datetime.strftime("%Y-%m-%d %H:%M:%S") # 格式化日期字符串
# 设置默认值
default_parser_id = "naive"
default_parser_config = json.dumps({
"layout_recognize": "DeepDOC",
"chunk_token_num": 512,
"delimiter": "\n!?;。;!?",
"auto_keywords": 0,
"auto_questions": 0,
"html4excel": False,
"raptor": {
"use_raptor": False
},
"graphrag": {
"use_graphrag": False
}
})
default_source_type = "local"
# 插入document表
doc_query = """
INSERT INTO document (
id, create_time, create_date, update_time, update_date,
thumbnail, kb_id, parser_id, parser_config, source_type,
type, created_by, name, location, size,
token_num, chunk_num, progress, progress_msg, process_begin_at,
process_duation, meta_fields, run, status
) VALUES (
%s, %s, %s, %s, %s,
%s, %s, %s, %s, %s,
%s, %s, %s, %s, %s,
%s, %s, %s, %s, %s,
%s, %s, %s, %s
)
"""
doc_params = [
doc_id, create_time, current_date, create_time, current_date, # ID和时间
None, kb_id, default_parser_id, default_parser_config, default_source_type, # thumbnail到source_type
file_type, created_by, file_name, file_location, file_size, # type到size
0, 0, 0.0, None, None, # token_num到process_begin_at
0.0, None, '0', '1' # process_duation到status
]
cursor.execute(doc_query, doc_params)
# 创建文件到文档的映射
f2d_id = generate_uuid()
f2d_query = """
INSERT INTO file2document (
id, create_time, create_date, update_time, update_date,
file_id, document_id
) VALUES (
%s, %s, %s, %s, %s,
%s, %s
)
"""
f2d_params = [
f2d_id, create_time, current_date, create_time, current_date,
file_id, doc_id
]
cursor.execute(f2d_query, f2d_params)
added_count += 1
# 更新知识库文档数量
if added_count > 0:
try:
update_query = """
UPDATE knowledgebase
SET doc_num = doc_num + %s,
update_date = %s
WHERE id = %s
"""
cursor.execute(update_query, (added_count, current_date, kb_id))
conn.commit() # 先提交更新操作
except Exception as e:
print(f"[WARNING] 更新知识库文档数量失败,但文档已添加: {str(e)}")
# 这里不抛出异常,因为文档已经添加成功
cursor.close()
conn.close()
return {
"added_count": added_count
}
except Exception as e:
print(f"[ERROR] 添加文档失败: {str(e)}")
print(f"[ERROR] 错误类型: {type(e)}")
import traceback
print(f"[ERROR] 堆栈信息: {traceback.format_exc()}")
raise Exception(f"添加文档到知识库失败: {str(e)}")
@classmethod
def delete_document(cls, doc_id):
"""删除文档"""
try:
conn = cls._get_db_connection()
cursor = conn.cursor()
# 先检查文档是否存在
check_query = "SELECT kb_id FROM document WHERE id = %s"
cursor.execute(check_query, (doc_id,))
result = cursor.fetchone()
if not result:
raise Exception("文档不存在")
kb_id = result[0]
# 删除文件到文档的映射
f2d_query = "DELETE FROM file2document WHERE document_id = %s"
cursor.execute(f2d_query, (doc_id,))
# 删除文档
doc_query = "DELETE FROM document WHERE id = %s"
cursor.execute(doc_query, (doc_id,))
# 更新知识库文档数量
update_query = """
UPDATE knowledgebase
SET doc_num = doc_num - 1,
update_date = %s
WHERE id = %s
"""
current_date = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
cursor.execute(update_query, (current_date, kb_id))
conn.commit()
cursor.close()
conn.close()
return True
except Exception as e:
print(f"[ERROR] 删除文档失败: {str(e)}")
raise Exception(f"删除文档失败: {str(e)}")

View File

@ -1,5 +1,6 @@
import uuid
import base64
from flask import jsonify
from Cryptodome.PublicKey import RSA
from Cryptodome.Cipher import PKCS1_v1_5
from werkzeug.security import generate_password_hash
@ -23,4 +24,23 @@ def rsa_psw(password: str) -> str:
# 加密密码
def encrypt_password(raw_password: str) -> str:
base64_password = base64.b64encode(raw_password.encode()).decode()
return generate_password_hash(base64_password)
return generate_password_hash(base64_password)
# 标准响应格式
def success_response(data=None, message="操作成功", code=0):
return jsonify({
"code": code,
"message": message,
"data": data
})
# 错误响应格式
def error_response(message="操作失败", code=500, details=None):
"""标准错误响应格式"""
response = {
"code": code,
"message": message
}
if details:
response["details"] = details
return jsonify(response), code if code >= 400 else 500

View File

@ -0,0 +1,128 @@
import { request } from "@/http/axios"
interface UploadResponse {
code: number
message?: string
data: any
}
// 获取文档列表
export function getDocumentListApi(params: {
kb_id: string
currentPage: number
size: number
name?: string
}) {
return request({
url: `/api/v1/knowledgebases/${params.kb_id}/documents`,
method: "get",
params: {
currentPage: params.currentPage,
size: params.size,
name: params.name
}
})
}
// 获取文档详情
export function getDocumentDetailApi(id: string) {
return request({
url: `/api/v1/documents/${id}`,
method: "get"
})
}
// 上传文档
export function uploadDocumentApi(formData: FormData): Promise<any> {
return request<UploadResponse>({
url: "/api/v1/knowledgebases/documents/upload",
method: "post",
data: formData,
headers: {
"Content-Type": "multipart/form-data"
}
}).then((response) => {
if (response.code !== 0) {
throw new Error(response.message || "上传失败")
}
return response.data
})
}
// 删除文档
export function deleteDocumentApi(docId: string) {
return request({
url: `/api/v1/knowledgebases/documents/${docId}`,
method: "delete"
})
}
// 批量删除文档
export function batchDeleteDocumentsApi(ids: string[]) {
return request({
url: "/api/v1/knowledgebases/documents/batch",
method: "delete",
data: { ids }
})
}
// 更改文档状态(启用/禁用)
export function changeDocumentStatusApi(id: string, status: string) {
return request({
url: `/api/v1/knowledgebases/documents/${id}/status`,
method: "put",
data: { status }
})
}
// 运行文档解析
export function runDocumentParseApi(id: string) {
return request({
url: `/api/v1/knowledgebases/documents/${id}/parse`,
method: "post"
})
}
// 获取文档分块列表
export function getDocumentChunksApi(params: {
doc_id: string
currentPage: number
size: number
content?: string
}) {
return request({
url: "/api/v1/chunks",
method: "get",
params
})
}
// 获取文件列表
export function getFileListApi(params: {
currentPage: number
size: number
name?: string
}) {
return request({
url: "/api/v1/files",
method: "get",
params
})
}
// 添加文档到知识库
export function addDocumentToKnowledgeBaseApi(data: {
kb_id: string
file_ids: string[]
}) {
return request<{ code: number, message?: string, data?: any }>({
url: `/api/v1/knowledgebases/${data.kb_id}/documents`,
method: "post",
data: { file_ids: data.file_ids }
}).then((response) => {
if (response.code === 0 || response.code === 201) {
return response.data || { added_count: data.file_ids.length }
}
throw new Error(response.message || "添加文档失败")
})
}

View File

@ -0,0 +1,79 @@
import { request } from "@/http/axios"
// 获取知识库列表
export function getKnowledgeBaseListApi(params: {
currentPage: number
size: number
name?: string
}) {
return request({
url: "/api/v1/knowledgebases",
method: "get",
params
})
}
// 获取知识库详情
export function getKnowledgeBaseDetailApi(id: string) {
return request({
url: `/api/v1/knowledgebases/${id}`,
method: "get"
})
}
// 创建知识库
export function createKnowledgeBaseApi(data: {
name: string
description?: string
language?: string
permission?: string
}) {
return request({
url: "/api/v1/knowledgebases",
method: "post",
data
})
}
// 更新知识库
export function updateKnowledgeBaseApi(id: string, data: {
name?: string
description?: string
language?: string
permission?: string
}) {
return request({
url: `/api/v1/knowledgebases/${id}`,
method: "put",
data
})
}
// 删除知识库
export function deleteKnowledgeBaseApi(id: string) {
return request({
url: `/api/v1/knowledgebases/${id}`,
method: "delete"
})
}
// 批量删除知识库
export function batchDeleteKnowledgeBaseApi(ids: string[]) {
return request({
url: "/api/v1/knowledgebases/batch",
method: "delete",
data: { ids }
})
}
// 添加文档到知识库
export function addDocumentToKnowledgeBaseApi(data: {
kb_id: string
file_ids: string[]
}) {
return request({
url: `/api/v1/knowledgebases/${data.kb_id}/documents`,
method: "post",
data: { file_ids: data.file_ids }
})
}

View File

@ -0,0 +1,63 @@
/**
*
*/
export interface FileData {
/** 文件ID */
id: string
/** 文件名称 */
name: string
/** 文件大小(字节) */
size: number
/** 文件类型 */
type: string
/** 知识库ID */
kb_id: string
/** 存储位置 */
location: string
/** 创建时间 */
create_time?: number
/** 更新时间 */
update_time?: number
}
/**
*
*/
export interface FileListResult {
/** 文件列表 */
list: FileData[]
/** 总条数 */
total: number
}
/**
*
*/
export interface PageQuery {
/** 当前页码 */
currentPage: number
/** 每页条数 */
size: number
}
/**
*
*/
export interface PageResult<T> {
/** 数据列表 */
list: T[]
/** 总条数 */
total: number
}
/**
*
*/
export interface ApiResponse<T> {
/** 状态码 */
code: number
/** 响应数据 */
data: T
/** 响应消息 */
message: string
}

View File

@ -1,25 +1,35 @@
<svg class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="200" height="200">
<!-- 文件夹主体 -->
<path d="M896 320H576l-64-64H256c-35.3 0-64 28.7-64 64v576c0 35.3 28.7 64 64 64h640c35.3 0 64-28.7 64-64V384c0-35.3-28.7-64-64-64z" fill="#4D90FE"></path>
<!-- 文件夹标签 -->
<path d="M896 320H576l-32-32H256c0-35.3 28.7-64 64-64h256l64 64h256c35.3 0 64 28.7 64 64H896z" fill="#3B78E7"></path>
<!-- 文件夹主体 -->
<!-- 文件1 -->
<path d="M384 512h256v64H384z" fill="#FFFFFF"></path>
<path d="M896 320H576l-64-64H256c-35.3 0-64 28.7-64 64v576c0 35.3 28.7 64 64 64h640c35.3 0 64-28.7 64-64V384c0-35.3-28.7-64-64-64z" fill="#4D90FE"></path>
<!-- 文件2 -->
<path d="M384 640h320v64H384z" fill="#FFFFFF"></path>
<!-- 文件夹标签 -->
<!-- 文件3 -->
<path d="M384 768h192v64H384z" fill="#FFFFFF"></path>
<path d="M896 320H576l-32-32H256c0-35.3 28.7-64 64-64h256l64 64h256c35.3 0 64 28.7 64 64H896z" fill="#3B78E7"></path>
<!-- 文件图标1 -->
<path d="M448 448h128v64H448z" fill="#F1F1F1"></path>
<!-- 文件1 -->
<!-- 文件图标2 -->
<path d="M320 576h128v64H320z" fill="#F1F1F1"></path>
<path d="M384 512h256v64H384z" fill="#FFFFFF"></path>
<!-- 文件图标3 -->
<path d="M512 704h128v64H512z" fill="#F1F1F1"></path>
</svg>
<!-- 文件2 -->
<path d="M384 640h320v64H384z" fill="#FFFFFF"></path>
<!-- 文件3 -->
<path d="M384 768h192v64H384z" fill="#FFFFFF"></path>
<!-- 文件图标1 -->
<path d="M448 448h128v64H448z" fill="#F1F1F1"></path>
<!-- 文件图标2 -->
<path d="M320 576h128v64H320z" fill="#F1F1F1"></path>
<!-- 文件图标3 -->
<path d="M512 704h128v64H512z" fill="#F1F1F1"></path>
</svg>

Before

Width:  |  Height:  |  Size: 930 B

After

Width:  |  Height:  |  Size: 907 B

View File

@ -0,0 +1,54 @@
<svg class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="200" height="200">
<!-- 书本主体 -->
<path d="
M256 128
h640
v768
h-640
q-64 0 -64 -64
v-640
q0 -64 64 -64
v768
h640
v-768
Z"
fill="#4D90FE"/>
<!-- 书页效果 -->
<path d="
M256 192
h576
v64
h-576
Z
M256 320
h576
v64
h-576
Z
M256 448
h576
v64
h-576
Z
M256 576
h576
v64
h-576
Z"
fill="#FFFFFF"/>
<!-- 书签 -->
<path d="
M768 128
l64 64
v128
l-64 -64
l-64 64
v-128
l64 -64
Z"
fill="#FF5252"/>
</svg>

After

Width:  |  Height:  |  Size: 677 B

View File

@ -1,9 +1,21 @@
<svg class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="200" height="200">
<path d="M924.8 385.6c-22.6-53.4-54.9-101.3-96-142.4-41.1-41.1-89-73.4-142.4-96C631.1 123.8 572.5 112 512 112s-119.1 11.8-174.4 35.2c-53.4 22.6-101.3 54.9-142.4 96-41.1 41.1-73.4 89-96 142.4C75.8 440.9 64 499.5 64 560c0 132.7 58.3 257.7 159.9 343.1l1.7 1.4c5.8 4.8 13.1 7.5 20.6 7.5h531.7c7.5 0 14.8-2.7 20.6-7.5l1.7-1.4C901.7 817.7 960 692.7 960 560c0-60.5-11.9-119.1-35.2-174.4zM761.4 836H262.6C184.5 765.5 140 665.6 140 560c0-99.4 38.7-192.8 109-263 70.3-70.3 163.7-109 263-109 99.4 0 192.8 38.7 263 109 70.3 70.3 109 163.7 109 263 0 105.6-44.5 205.5-122.6 276z" p-id="2001"></path>
<path d="M512 400c-26.5 0-48 21.5-48 48s21.5 48 48 48 48-21.5 48-48-21.5-48-48-48z" p-id="2002"></path>
<path d="M352 480c-26.5 0-48 21.5-48 48s21.5 48 48 48 48-21.5 48-48-21.5-48-48-48z" p-id="2003"></path>
<path d="M672 480c-26.5 0-48 21.5-48 48s21.5 48 48 48 48-21.5 48-48-21.5-48-48-48z" p-id="2004"></path>
<path d="M512 544c-42.1 0-76.9 32.3-80.7 73.4-1.3 14.6 9.6 27.6 24.2 28.9 0.8 0.1 1.6 0.1 2.4 0.1 13.6 0 25.1-10.3 26.5-24.1 1.1-12.3 11.4-21.7 23.8-22.3 0.6 0 1.3-0.1 1.9-0.1 13.8 0 25 11.2 25 25v90c0 14.9 12.1 27 27 27s27-12.1 27-27v-90c0-42.1-34.1-76.2-76.2-76.2z" p-id="2005"></path>
<path d="M352 624c-42.1 0-76.9 32.3-80.7 73.4-1.3 14.6 9.6 27.6 24.2 28.9 0.8 0.1 1.6 0.1 2.4 0.1 13.6 0 25.1-10.3 26.5-24.1 1.1-12.3 11.4-21.7 23.8-22.3 0.6 0 1.3-0.1 1.9-0.1 13.8 0 25 11.2 25 25v30c0 14.9 12.1 27 27 27s27-12.1 27-27v-30c0-42.1-34.1-76.2-76.2-76.2z" p-id="2006"></path>
<path d="M672 624c-42.1 0-76.9 32.3-80.7 73.4-1.3 14.6 9.6 27.6 24.2 28.9 0.8 0.1 1.6 0.1 2.4 0.1 13.6 0 25.1-10.3 26.5-24.1 1.1-12.3 11.4-21.7 23.8-22.3 0.6 0 1.3-0.1 1.9-0.1 13.8 0 25 11.2 25 25v30c0 14.9 12.1 27 27 27s27-12.1 27-27v-30c0-42.1-34.1-76.2-76.2-76.2z" p-id="2007"></path>
<!-- 中心领导者 -->
<path d="M512 256a128 128 0 1 1 0 256 128 128 0 0 1 0-256z" fill="#4D90FE"/>
<path d="M416 512h192l48 320H368z" fill="#4D90FE"/>
<!-- 左侧成员 -->
<path d="M256 384a96 96 0 1 1 0 192 96 96 0 0 1 0-192z" fill="#3B78E7"/>
<path d="M192 576h128l32 256H160z" fill="#3B78E7"/>
<!-- 右侧成员 -->
<path d="M768 384a96 96 0 1 1 0 192 96 96 0 0 1 0-192z" fill="#3B78E7"/>
<path d="M704 576h128l32 256H672z" fill="#3B78E7"/>
<!-- 连接弧线 -->
<path d="M320 448Q416 384 512 384T704 448"
stroke="#4D90FE"
stroke-width="16"
fill="none"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 711 B

View File

@ -1,9 +1,13 @@
<svg class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="200" height="200">
<path d="M924.8 385.6c-22.6-53.4-54.9-101.3-96-142.4-41.1-41.1-89-73.4-142.4-96C631.1 123.8 572.5 112 512 112s-119.1 11.8-174.4 35.2c-53.4 22.6-101.3 54.9-142.4 96-41.1 41.1-73.4 89-96 142.4C75.8 440.9 64 499.5 64 560c0 132.7 58.3 257.7 159.9 343.1l1.7 1.4c5.8 4.8 13.1 7.5 20.6 7.5h531.7c7.5 0 14.8-2.7 20.6-7.5l1.7-1.4C901.7 817.7 960 692.7 960 560c0-60.5-11.9-119.1-35.2-174.4zM761.4 836H262.6C184.5 765.5 140 665.6 140 560c0-99.4 38.7-192.8 109-263 70.3-70.3 163.7-109 263-109 99.4 0 192.8 38.7 263 109 70.3 70.3 109 163.7 109 263 0 105.6-44.5 205.5-122.6 276z" p-id="3001"></path>
<path d="M512 320c-79.5 0-144 64.5-144 144s64.5 144 144 144 144-64.5 144-144-64.5-144-144-144z m0 224c-44.2 0-80-35.8-80-80s35.8-80 80-80 80 35.8 80 80-35.8 80-80 80z" p-id="3002"></path>
<path d="M704 608h-32c-4.4 0-8 3.6-8 8v80c0 4.4 3.6 8 8 8h32c4.4 0 8-3.6 8-8v-80c0-4.4-3.6-8-8-8z" p-id="3003"></path>
<path d="M592 608h-32c-4.4 0-8 3.6-8 8v80c0 4.4 3.6 8 8 8h32c4.4 0 8-3.6 8-8v-80c0-4.4-3.6-8-8-8z" p-id="3004"></path>
<path d="M480 608h-32c-4.4 0-8 3.6-8 8v80c0 4.4 3.6 8 8 8h32c4.4 0 8-3.6 8-8v-80c0-4.4-3.6-8-8-8z" p-id="3005"></path>
<path d="M368 608h-32c-4.4 0-8 3.6-8 8v80c0 4.4 3.6 8 8 8h32c4.4 0 8-3.6 8-8v-80c0-4.4-3.6-8-8-8z" p-id="3006"></path>
<path d="M704 736h-368c-4.4 0-8 3.6-8 8v32c0 4.4 3.6 8 8 8h368c4.4 0 8-3.6 8-8v-32c0-4.4-3.6-8-8-8z" p-id="3007"></path>
<!-- 完全居中的空心齿轮 -->
<path d="
M512 128
l112 0 l56 168 l168 -56 l80 80 l-56 168 l168 56 l0 112 l-168 56 l56 168 l-80 80 l-168 -56 l-56 168 l-112 0 l-56 -168 l-168 56 l-80 -80 l56 -168 l-168 -56 l0 -112 l168 -56 l-56 -168 l80 -80 l168 56 l56 -168
M512 320
a192 192 0 1 0 0 384
a192 192 0 1 0 0 -384
Z"
fill="#4D90FE"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 488 B

View File

@ -24,6 +24,7 @@ function createInstance() {
// 响应拦截器(可根据具体业务作出相应的调整)
instance.interceptors.response.use(
(response) => {
// console.log("API Response:", response)
// apiData 是 api 返回的数据
const apiData = response.data
// 二进制数据则直接返回
@ -101,6 +102,7 @@ function createInstance() {
function createRequest(instance: AxiosInstance) {
return <T>(config: AxiosRequestConfig): Promise<T> => {
const token = getToken()
// console.log("Request config:", config)
// 默认配置
const defaultConfig: AxiosRequestConfig = {
// 接口地址

View File

@ -0,0 +1,955 @@
<script lang="ts" setup>
import type { FormInstance } from "element-plus"
import { log } from "node:console"
import {
deleteDocumentApi,
getDocumentListApi,
getFileListApi,
runDocumentParseApi,
uploadDocumentApi
} from "@@/apis/kbs/document"
import {
addDocumentToKnowledgeBaseApi,
batchDeleteKnowledgeBaseApi,
createKnowledgeBaseApi,
deleteKnowledgeBaseApi,
getKnowledgeBaseListApi
} from "@@/apis/kbs/knowledgebase"
import { usePagination } from "@@/composables/usePagination"
import { CaretRight, Delete, Plus, Refresh, Search, View } from "@element-plus/icons-vue"
import axios from "axios"
import { ElMessage, ElMessageBox } from "element-plus"
import { onActivated, onMounted, reactive, ref, watch } from "vue"
import "element-plus/dist/index.css"
import "element-plus/theme-chalk/el-message-box.css"
import "element-plus/theme-chalk/el-message.css"
defineOptions({
//
name: "KnowledgeBase"
})
const loading = ref<boolean>(false)
const { paginationData, handleCurrentChange, handleSizeChange } = usePagination()
const createDialogVisible = ref(false)
const uploadLoading = ref(false)
//
interface KnowledgeBaseData {
id: string
name: string
description: string
doc_num: number
create_time: number
create_date: string
avatar?: string
language: string
permission: string
chunk_num: number
token_num: number
}
//
const knowledgeBaseForm = reactive({
name: "",
description: "",
language: "Chinese",
permission: "me"
})
// API
interface FileListResponse {
list: any[]
total: number
}
interface ApiResponse<T> {
data: T
code: number
message: string
}
interface ListResponse {
list: any[]
total: number
}
//
const knowledgeBaseFormRules = {
name: [
{ required: true, message: "请输入知识库名称", trigger: "blur" },
{ min: 2, max: 50, message: "长度在 2 到 50 个字符", trigger: "blur" }
],
description: [
{ max: 200, message: "描述不能超过200个字符", trigger: "blur" }
]
}
const knowledgeBaseFormRef = ref<FormInstance | null>(null)
//
const tableData = ref<KnowledgeBaseData[]>([])
const searchFormRef = ref<FormInstance | null>(null)
const searchData = reactive({
name: ""
})
//
const multipleSelection = ref<KnowledgeBaseData[]>([])
//
function getTableData() {
loading.value = true
// API
getKnowledgeBaseListApi({
currentPage: paginationData.currentPage,
size: paginationData.pageSize,
name: searchData.name
}).then((response) => {
const result = response as ApiResponse<ListResponse>
paginationData.total = result.data.total
tableData.value = result.data.list
//
multipleSelection.value = []
}).catch(() => {
tableData.value = []
}).finally(() => {
loading.value = false
})
}
//
function handleSearch() {
paginationData.currentPage === 1 ? getTableData() : (paginationData.currentPage = 1)
}
//
function resetSearch() {
searchFormRef.value?.resetFields()
handleSearch()
}
//
function handleCreate() {
createDialogVisible.value = true
}
//
async function submitCreate() {
if (!knowledgeBaseFormRef.value) return
await knowledgeBaseFormRef.value.validate(async (valid) => {
if (valid) {
uploadLoading.value = true
try {
await createKnowledgeBaseApi(knowledgeBaseForm)
ElMessage.success("知识库创建成功")
getTableData()
createDialogVisible.value = false
//
knowledgeBaseFormRef.value?.resetFields()
} catch (error: unknown) {
let errorMessage = "创建失败"
if (error instanceof Error) {
errorMessage += `: ${error.message}`
}
ElMessage.error(errorMessage)
} finally {
uploadLoading.value = false
}
}
})
}
//
const viewDialogVisible = ref(false)
const currentKnowledgeBase = ref<KnowledgeBaseData | null>(null)
const documentLoading = ref(false)
const documentList = ref<any[]>([])
//
const docPaginationData = reactive({
currentPage: 1,
pageSize: 10,
total: 0,
pageSizes: [10, 20, 50, 100],
layout: "total, sizes, prev, pager, next, jumper"
})
//
function handleDocCurrentChange(page: number) {
docPaginationData.currentPage = page
getDocumentList()
}
function handleDocSizeChange(size: number) {
docPaginationData.pageSize = size
docPaginationData.currentPage = 1
getDocumentList()
}
//
function getDocumentList() {
if (!currentKnowledgeBase.value) return
documentLoading.value = true
getDocumentListApi({
kb_id: currentKnowledgeBase.value.id,
currentPage: docPaginationData.currentPage,
size: docPaginationData.pageSize,
name: ""
}).then((response) => {
const result = response as ApiResponse<ListResponse>
documentList.value = result.data.list
docPaginationData.total = result.data.total
}).catch((error) => {
ElMessage.error(`获取文档列表失败: ${error?.message || "未知错误"}`)
documentList.value = []
}).finally(() => {
documentLoading.value = false
})
}
// handleView
function handleView(row: KnowledgeBaseData) {
currentKnowledgeBase.value = row
viewDialogVisible.value = true
//
docPaginationData.currentPage = 1
//
getDocumentList()
}
//
function formatParseStatus(progress: number) {
if (progress === 0) return "未解析"
if (progress === 1) return "已完成"
return `解析中 ${Math.floor(progress * 100)}%`
}
//
function getParseStatusType(progress: number) {
if (progress === 0) return "info"
if (progress === 1) return "success"
return "warning"
}
//
function handleParseDocument(row: any) {
//
if (row.progress === 1) {
ElMessage.warning("文档已完成解析,无需再重复解析")
return
}
ElMessageBox.confirm(
`确定要解析文档 "${row.name}" 吗?`,
"解析确认",
{
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "info"
}
).then(() => {
runDocumentParseApi(row.id)
.then(() => {
ElMessage.success("解析任务已提交")
//
getDocumentList()
})
.catch((error) => {
ElMessage.error(`解析任务提交失败: ${error?.message || "未知错误"}`)
})
}).catch(() => {
//
})
}
//
function handleRemoveDocument(row: any) {
ElMessageBox.confirm(
`确定要从知识库中移除文档 "${row.name}" 吗?<br><span style="color: #909399; font-size: 12px;">该操作只是移除知识库文件,不会删除原始文件</span>`,
"移除确认",
{
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
dangerouslyUseHTMLString: true
}
).then(() => {
deleteDocumentApi(row.id)
.then(() => {
ElMessage.success("文档已从知识库移除")
//
getDocumentList()
//
getTableData()
})
.catch((error) => {
ElMessage.error(`移除文档失败: ${error?.message || "未知错误"}`)
})
}).catch(() => {
//
})
}
//
const addDocumentDialogVisible = ref(false)
const selectedFiles = ref<string[]>([])
const fileLoading = ref(false)
const fileList = ref<any[]>([])
const filePaginationData = reactive({
currentPage: 1,
pageSize: 10,
total: 0,
pageSizes: [10, 20, 50, 100],
layout: "total, sizes, prev, pager, next, jumper"
})
//
function handleAddDocument() {
addDocumentDialogVisible.value = true
//
selectedFiles.value = []
//
getFileList()
}
//
function getFileList() {
fileLoading.value = true
// API
getFileListApi({
currentPage: filePaginationData.currentPage,
size: filePaginationData.pageSize,
name: ""
}).then((response) => {
const typedResponse = response as ApiResponse<FileListResponse>
fileList.value = typedResponse.data.list
filePaginationData.total = typedResponse.data.total
}).catch((error) => {
ElMessage.error(`获取文件列表失败: ${error?.message || "未知错误"}`)
fileList.value = []
}).finally(() => {
fileLoading.value = false
})
}
//
function handleFileSelectionChange(selection: any[]) {
// 使Array.fromJSON
selectedFiles.value = JSON.parse(JSON.stringify(Array.from(selection).map(item => item.id)))
}
//
const isAddingDocument = ref(false)
const messageShown = ref(false) // messageShown
// confirmAddDocument
async function confirmAddDocument() {
//
if (isAddingDocument.value) {
console.log("正在处理添加文档请求,请勿重复点击")
return
}
if (selectedFiles.value.length === 0) {
ElMessage.warning("请至少选择一个文件")
return
}
if (!currentKnowledgeBase.value) return
try {
//
isAddingDocument.value = true
messageShown.value = false // 使
console.log("开始添加文档请求...", selectedFiles.value)
// ID
const fileIds = JSON.parse(JSON.stringify([...selectedFiles.value]))
// API - try/catch
const response = await axios.post(
`/api/v1/knowledgebases/${currentKnowledgeBase.value.id}/documents`,
{ file_ids: fileIds }
)
console.log("API原始响应:", response)
//
if (response.data && (response.data.code === 0 || response.data.code === 201)) {
//
if (!messageShown.value) {
messageShown.value = true
console.log("显示成功消息")
ElMessage.success("文档添加成功")
}
addDocumentDialogVisible.value = false
getDocumentList()
getTableData()
} else {
//
throw new Error(response.data?.message || "添加文档失败")
}
} catch (error: any) {
// API
console.error("API请求失败详情:", {
error: error?.toString(),
stack: error?.stack,
response: error?.response?.data,
request: error?.request,
config: error?.config
})
//
console.log("错误详情:", error)
if (error.response) {
console.log("响应数据:", error.response.data)
console.log("响应状态:", error.response.status)
}
ElMessage.error(`添加文档失败: ${error?.message || "未知错误"}`)
} finally {
//
console.log("添加文档请求完成,解除锁定", new Date().toISOString())
setTimeout(() => {
isAddingDocument.value = false
}, 500) //
}
}
//
function formatFileSize(size: number) {
if (!size) return "0 B"
const units = ["B", "KB", "MB", "GB", "TB"]
let index = 0
while (size >= 1024 && index < units.length - 1) {
size /= 1024
index++
}
return `${size.toFixed(2)} ${units[index]}`
}
//
function formatFileType(type: string) {
const typeMap: Record<string, string> = {
pdf: "PDF",
doc: "Word",
docx: "Word",
xls: "Excel",
xlsx: "Excel",
ppt: "PPT",
pptx: "PPT",
txt: "文本",
md: "Markdown",
jpg: "图片",
jpeg: "图片",
png: "图片"
}
return typeMap[type.toLowerCase()] || type
}
//
function handleDelete(row: KnowledgeBaseData) {
ElMessageBox.confirm(
`确定要删除知识库 "${row.name}" 吗?删除后将无法恢复,且其中的所有文档也将被删除。`,
"删除确认",
{
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
dangerouslyUseHTMLString: true,
center: true,
customClass: "delete-confirm-dialog",
distinguishCancelAndClose: true,
showClose: false,
closeOnClickModal: false,
closeOnPressEscape: true,
roundButton: true,
beforeClose: (action, instance, done) => {
if (action === "confirm") {
instance.confirmButtonLoading = true
instance.confirmButtonText = "删除中..."
loading.value = true
deleteKnowledgeBaseApi(row.id)
.then(() => {
ElMessage.success("删除成功")
getTableData() //
done()
})
.catch((error) => {
ElMessage.error(`删除失败: ${error?.message || "未知错误"}`)
done()
})
.finally(() => {
instance.confirmButtonLoading = false
loading.value = false
})
} else {
done()
}
}
}
).catch(() => {
//
})
}
//
function handleBatchDelete() {
if (multipleSelection.value.length === 0) {
ElMessage.warning("请至少选择一个知识库")
return
}
ElMessageBox.confirm(
`确定要删除选中的 <strong>${multipleSelection.value.length}</strong> 个知识库吗?<br><span style="color: #F56C6C; font-size: 12px;">此操作不可恢复,且其中的所有文档也将被删除</span>`,
"批量删除确认",
{
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
dangerouslyUseHTMLString: true,
center: true,
customClass: "delete-confirm-dialog",
distinguishCancelAndClose: true,
showClose: false,
closeOnClickModal: false,
closeOnPressEscape: true,
roundButton: true,
beforeClose: (action, instance, done) => {
if (action === "confirm") {
instance.confirmButtonLoading = true
instance.confirmButtonText = "删除中..."
loading.value = true
const ids = multipleSelection.value.map(item => item.id)
batchDeleteKnowledgeBaseApi(ids)
.then(() => {
ElMessage.success(`成功删除 ${multipleSelection.value.length} 个知识库`)
getTableData() //
done()
})
.catch((error) => {
ElMessage.error(`批量删除失败: ${error?.message || "未知错误"}`)
done()
})
.finally(() => {
instance.confirmButtonLoading = false
loading.value = false
})
} else {
done()
}
}
}
).catch(() => {
//
})
}
//
function handleSelectionChange(selection: KnowledgeBaseData[]) {
multipleSelection.value = selection
}
//
watch([() => paginationData.currentPage, () => paginationData.pageSize], getTableData, { immediate: true })
//
onMounted(() => {
getTableData()
})
//
onActivated(() => {
getTableData()
})
</script>
<template>
<div class="app-container">
<el-card v-loading="loading" shadow="never" class="search-wrapper">
<el-form ref="searchFormRef" :inline="true" :model="searchData">
<el-form-item prop="name" label="知识库名称">
<el-input v-model="searchData.name" placeholder="请输入" />
</el-form-item>
<el-form-item>
<el-button type="primary" :icon="Search" @click="handleSearch">
搜索
</el-button>
<el-button :icon="Refresh" @click="resetSearch">
重置
</el-button>
</el-form-item>
</el-form>
</el-card>
<el-card v-loading="loading" shadow="never">
<div class="toolbar-wrapper">
<div>
<el-button
type="primary"
:icon="Plus"
@click="handleCreate"
>
新建知识库
</el-button>
<el-button
type="danger"
:icon="Delete"
:disabled="multipleSelection.length === 0"
@click="handleBatchDelete"
>
批量删除
</el-button>
</div>
</div>
<div class="table-wrapper">
<el-table :data="tableData" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="50" align="center" />
<el-table-column label="序号" align="center" width="80">
<template #default="scope">
{{ (paginationData.currentPage - 1) * paginationData.pageSize + scope.$index + 1 }}
</template>
</el-table-column>
<el-table-column prop="name" label="知识库名称" align="center" min-width="120" />
<el-table-column prop="description" label="描述" align="center" min-width="180" show-overflow-tooltip />
<el-table-column prop="doc_num" label="文档数量" align="center" width="100" />
<!-- 添加语言列 -->
<el-table-column label="语言" align="center" width="100">
<template #default="scope">
<el-tag type="info" size="small">
{{ scope.row.language === 'Chinese' ? '中文' : '英文' }}
</el-tag>
</template>
</el-table-column>
<!-- 添加权限列 -->
<el-table-column label="权限" align="center" width="100">
<template #default="scope">
<el-tag :type="scope.row.permission === 'me' ? 'success' : 'warning'" size="small">
{{ scope.row.permission === 'me' ? '个人' : '团队' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="创建时间" align="center" width="180">
<template #default="scope">
{{ scope.row.create_date }}
</template>
</el-table-column>
<el-table-column fixed="right" label="操作" width="180" align="center">
<template #default="scope">
<el-button
type="primary"
text
bg
size="small"
:icon="View"
@click="handleView(scope.row)"
>
查看
</el-button>
<el-button
type="danger"
text
bg
size="small"
:icon="Delete"
@click="handleDelete(scope.row)"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
</div>
<div class="pager-wrapper">
<el-pagination
background
:layout="paginationData.layout"
:page-sizes="paginationData.pageSizes"
:total="paginationData.total"
:page-size="paginationData.pageSize"
:current-page="paginationData.currentPage"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</el-card>
<!-- 知识库详情对话框 -->
<el-dialog
v-model="viewDialogVisible"
:title="`知识库详情 - ${currentKnowledgeBase?.name || ''}`"
width="80%"
>
<div v-if="currentKnowledgeBase">
<div class="kb-info-header">
<div>
<span class="kb-info-label">知识库ID:</span> {{ currentKnowledgeBase.id }}
</div>
<div>
<span class="kb-info-label">文档总数:</span> {{ currentKnowledgeBase.doc_num }}
</div>
<div>
<span class="kb-info-label">语言:</span>
<el-tag type="info" size="small">
{{ currentKnowledgeBase.language === 'Chinese' ? '中文' : '英文' }}
</el-tag>
</div>
<div>
<span class="kb-info-label">权限:</span>
<el-tag :type="currentKnowledgeBase.permission === 'me' ? 'success' : 'warning'" size="small">
{{ currentKnowledgeBase.permission === 'me' ? '个人' : '团队' }}
</el-tag>
</div>
</div>
<div class="document-table-header">
<div class="left-buttons">
<el-button type="primary" @click="handleAddDocument">
添加文档
</el-button>
</div>
</div>
<div class="document-table-wrapper" v-loading="documentLoading">
<el-table :data="documentList" style="width: 100%">
<el-table-column prop="name" label="名称" min-width="180" show-overflow-tooltip />
<el-table-column prop="chunk_num" label="分块数" width="100" align="center" />
<el-table-column label="上传日期" width="180" align="center">
<template #default="scope">
{{ scope.row.create_date }}
</template>
</el-table-column>
<el-table-column label="解析状态" width="120" align="center">
<template #default="scope">
<el-tag :type="getParseStatusType(scope.row.progress)">
{{ formatParseStatus(scope.row.progress) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="200" align="center">
<template #default="scope">
<el-button
type="success"
size="small"
:icon="CaretRight"
@click="handleParseDocument(scope.row)"
>
解析
</el-button>
<el-button
type="danger"
size="small"
:icon="Delete"
@click="handleRemoveDocument(scope.row)"
>
移除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页控件 -->
<div class="pagination-container">
<el-pagination
v-model:current-page="docPaginationData.currentPage"
v-model:page-size="docPaginationData.pageSize"
:page-sizes="docPaginationData.pageSizes"
:layout="docPaginationData.layout"
:total="docPaginationData.total"
@size-change="handleDocSizeChange"
@current-change="handleDocCurrentChange"
/>
</div>
</div>
</div>
</el-dialog>
<!-- 新建知识库对话框 -->
<el-dialog
v-model="createDialogVisible"
title="新建知识库"
width="40%"
>
<el-form
ref="knowledgeBaseFormRef"
:model="knowledgeBaseForm"
:rules="knowledgeBaseFormRules"
label-width="100px"
>
<el-form-item label="知识库名称" prop="name">
<el-input v-model="knowledgeBaseForm.name" placeholder="请输入知识库名称" />
</el-form-item>
<el-form-item label="描述" prop="description">
<el-input
v-model="knowledgeBaseForm.description"
type="textarea"
:rows="3"
placeholder="请输入知识库描述"
/>
</el-form-item>
<el-form-item label="语言" prop="language">
<el-select v-model="knowledgeBaseForm.language" placeholder="请选择语言">
<el-option label="中文" value="Chinese" />
<el-option label="英文" value="English" />
</el-select>
</el-form-item>
<el-form-item label="权限" prop="permission">
<el-select v-model="knowledgeBaseForm.permission" placeholder="请选择权限">
<el-option label="个人" value="me" />
<el-option label="团队" value="team" />
</el-select>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="createDialogVisible = false">
取消
</el-button>
<el-button
type="primary"
:loading="uploadLoading"
@click="submitCreate"
>
确认创建
</el-button>
</template>
</el-dialog>
<!-- 添加文档对话框 -->
<el-dialog
v-model="addDocumentDialogVisible"
title="添加文档到知识库"
width="70%"
>
<div v-loading="fileLoading">
<el-table
:data="fileList"
style="width: 100%"
@selection-change="handleFileSelectionChange"
>
<el-table-column type="selection" width="55" />
<el-table-column prop="name" label="文件名" min-width="180" show-overflow-tooltip />
<el-table-column prop="size" label="大小" width="100" align="center">
<template #default="scope">
{{ formatFileSize(scope.row.size) }}
</template>
</el-table-column>
<el-table-column prop="type" label="类型" width="100" align="center">
<template #default="scope">
{{ formatFileType(scope.row.type) }}
</template>
</el-table-column>
<!-- 移除上传日期列 -->
</el-table>
<!-- 分页控件 -->
<div class="pagination-container">
<el-pagination
v-model:current-page="filePaginationData.currentPage"
v-model:page-size="filePaginationData.pageSize"
:page-sizes="filePaginationData.pageSizes"
:layout="filePaginationData.layout"
:total="filePaginationData.total"
@size-change="(size) => { filePaginationData.pageSize = size; filePaginationData.currentPage = 1; getFileList(); }"
@current-change="(page) => { filePaginationData.currentPage = page; getFileList(); }"
/>
</div>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="addDocumentDialogVisible = false">取消</el-button>
<el-button
type="primary"
:disabled="isAddingDocument"
@click.stop.prevent="confirmAddDocument"
>
{{ isAddingDocument ? '处理中...' : '确定' }}
</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<style lang="scss" scoped>
.el-alert {
margin-bottom: 20px;
}
.search-wrapper {
margin-bottom: 20px;
:deep(.el-card__body) {
padding-bottom: 2px;
}
}
.toolbar-wrapper {
display: flex;
justify-content: space-between;
margin-bottom: 20px;
}
.table-wrapper {
margin-bottom: 20px;
}
.pager-wrapper {
display: flex;
justify-content: flex-end;
}
.document-table-header {
display: flex;
justify-content: flex-end;
margin-bottom: 16px;
margin-top: 16px;
}
.kb-info-header {
display: flex;
flex-wrap: wrap;
gap: 20px;
margin-bottom: 20px;
padding: 16px;
background-color: #f5f7fa;
border-radius: 4px;
.kb-info-label {
color: #606266;
font-weight: 500;
margin-right: 8px;
}
}
.document-table-wrapper {
margin-top: 20px;
}
.document-table-header {
display: flex;
justify-content: flex-start;
margin-bottom: 16px;
margin-top: 16px;
}
.pagination-container {
margin-top: 20px;
display: flex;
justify-content: flex-end;
}
.delete-confirm-dialog {
:deep(.el-message-box__message) {
text-align: center;
}
}
</style>

View File

@ -116,127 +116,25 @@ export const constantRoutes: RouteRecordRaw[] = [
}
}
]
},
{
path: "/kb",
component: Layouts,
redirect: "/kb/index",
children: [
{
path: "index",
component: () => import("@/pages/knowledgebase/index.vue"),
name: "KB",
meta: {
title: "知识库管理",
svgIcon: "kb",
affix: false,
keepAlive: true
}
}
]
}
// {
// path: "/",
// component: () => import("@/pages/demo/element-plus/index.vue"),
// name: "ElementPlus",
// meta: {
// title: "Element Plus",
// keepAlive: true
// }
// }
// {
// path: "/demo",
// component: Layouts,
// redirect: "/demo/unocss",
// name: "Demo",
// meta: {
// title: "示例集合",
// elIcon: "DataBoard"
// },
// children: [
// {
// path: "unocss",
// component: () => import("@/pages/demo/unocss/index.vue"),
// name: "UnoCSS",
// meta: {
// title: "UnoCSS"
// }
// },
// {
// path: "vxe-table",
// component: () => import("@/pages/demo/vxe-table/index.vue"),
// name: "VxeTable",
// meta: {
// title: "Vxe Table",
// keepAlive: true
// }
// },
// {
// path: "level2",
// component: () => import("@/pages/demo/level2/index.vue"),
// redirect: "/demo/level2/level3",
// name: "Level2",
// meta: {
// title: "二级路由",
// alwaysShow: true
// },
// children: [
// {
// path: "level3",
// component: () => import("@/pages/demo/level2/level3/index.vue"),
// name: "Level3",
// meta: {
// title: "三级路由",
// keepAlive: true
// }
// }
// ]
// },
// {
// path: "composable-demo",
// redirect: "/demo/composable-demo/use-fetch-select",
// name: "ComposableDemo",
// meta: {
// title: "组合式函数"
// },
// children: [
// {
// path: "use-fetch-select",
// component: () => import("@/pages/demo/composable-demo/use-fetch-select.vue"),
// name: "UseFetchSelect",
// meta: {
// title: "useFetchSelect"
// }
// },
// {
// path: "use-fullscreen-loading",
// component: () => import("@/pages/demo/composable-demo/use-fullscreen-loading.vue"),
// name: "UseFullscreenLoading",
// meta: {
// title: "useFullscreenLoading"
// }
// },
// {
// path: "use-watermark",
// component: () => import("@/pages/demo/composable-demo/use-watermark.vue"),
// name: "UseWatermark",
// meta: {
// title: "useWatermark"
// }
// }
// ]
// }
// ]
// },
// {
// path: "/link",
// meta: {
// title: "文档链接",
// elIcon: "Link"
// },
// children: [
// {
// path: "https://juejin.cn/post/7445151895121543209",
// component: () => {},
// name: "Link1",
// meta: {
// title: "中文文档"
// }
// },
// {
// path: "https://juejin.cn/column/7207659644487139387",
// component: () => {},
// name: "Link2",
// meta: {
// title: "新手教程"
// }
// }
// ]
// }
]
/**

View File

@ -8,6 +8,7 @@ export {}
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
ConfirmDialog: typeof import('./../../src/components/ConfirmDialog.vue')['default']
ElAside: typeof import('element-plus/es')['ElAside']
ElAvatar: typeof import('element-plus/es')['ElAvatar']
ElBacktop: typeof import('element-plus/es')['ElBacktop']
@ -18,6 +19,8 @@ declare module 'vue' {
ElCard: typeof import('element-plus/es')['ElCard']
ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
ElContainer: typeof import('element-plus/es')['ElContainer']
ElDescriptions: typeof import('element-plus/es')['ElDescriptions']
ElDescriptionsItem: typeof import('element-plus/es')['ElDescriptionsItem']
ElDialog: typeof import('element-plus/es')['ElDialog']
ElDivider: typeof import('element-plus/es')['ElDivider']
ElDrawer: typeof import('element-plus/es')['ElDrawer']

View File

@ -9,18 +9,18 @@ declare module 'vue' {
export interface GlobalComponents {
SvgIcon: import("vue").DefineComponent<{
name: {
type: import("vue").PropType<"dashboard" | "file" | "fullscreen-exit" | "fullscreen" | "keyboard-down" | "keyboard-enter" | "keyboard-esc" | "keyboard-up" | "search" | "team-management" | "user-config" | "user-management">;
type: import("vue").PropType<"dashboard" | "file" | "fullscreen-exit" | "fullscreen" | "kb" | "keyboard-down" | "keyboard-enter" | "keyboard-esc" | "keyboard-up" | "search" | "team-management" | "user-config" | "user-management">;
default: string;
required: true;
};
}, {}, unknown, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").VNodeProps & import("vue").AllowedComponentProps & import("vue").ComponentCustomProps, Readonly<import("vue").ExtractPropTypes<{
name: {
type: import("vue").PropType<"dashboard" | "file" | "fullscreen-exit" | "fullscreen" | "keyboard-down" | "keyboard-enter" | "keyboard-esc" | "keyboard-up" | "search" | "team-management" | "user-config" | "user-management">;
type: import("vue").PropType<"dashboard" | "file" | "fullscreen-exit" | "fullscreen" | "kb" | "keyboard-down" | "keyboard-enter" | "keyboard-esc" | "keyboard-up" | "search" | "team-management" | "user-config" | "user-management">;
default: string;
required: true;
};
}>>, {
name: "dashboard" | "file" | "fullscreen-exit" | "fullscreen" | "keyboard-down" | "keyboard-enter" | "keyboard-esc" | "keyboard-up" | "search" | "team-management" | "user-config" | "user-management";
name: "dashboard" | "file" | "fullscreen-exit" | "fullscreen" | "kb" | "keyboard-down" | "keyboard-enter" | "keyboard-esc" | "keyboard-up" | "search" | "team-management" | "user-config" | "user-management";
}>;
}
}

View File

@ -7,20 +7,20 @@
declare module '~virtual/svg-component' {
const SvgIcon: import("vue").DefineComponent<{
name: {
type: import("vue").PropType<"dashboard" | "file" | "fullscreen-exit" | "fullscreen" | "keyboard-down" | "keyboard-enter" | "keyboard-esc" | "keyboard-up" | "search" | "team-management" | "user-config" | "user-management">;
type: import("vue").PropType<"dashboard" | "file" | "fullscreen-exit" | "fullscreen" | "kb" | "keyboard-down" | "keyboard-enter" | "keyboard-esc" | "keyboard-up" | "search" | "team-management" | "user-config" | "user-management">;
default: string;
required: true;
};
}, {}, unknown, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").VNodeProps & import("vue").AllowedComponentProps & import("vue").ComponentCustomProps, Readonly<import("vue").ExtractPropTypes<{
name: {
type: import("vue").PropType<"dashboard" | "file" | "fullscreen-exit" | "fullscreen" | "keyboard-down" | "keyboard-enter" | "keyboard-esc" | "keyboard-up" | "search" | "team-management" | "user-config" | "user-management">;
type: import("vue").PropType<"dashboard" | "file" | "fullscreen-exit" | "fullscreen" | "kb" | "keyboard-down" | "keyboard-enter" | "keyboard-esc" | "keyboard-up" | "search" | "team-management" | "user-config" | "user-management">;
default: string;
required: true;
};
}>>, {
name: "dashboard" | "file" | "fullscreen-exit" | "fullscreen" | "keyboard-down" | "keyboard-enter" | "keyboard-esc" | "keyboard-up" | "search" | "team-management" | "user-config" | "user-management";
name: "dashboard" | "file" | "fullscreen-exit" | "fullscreen" | "kb" | "keyboard-down" | "keyboard-enter" | "keyboard-esc" | "keyboard-up" | "search" | "team-management" | "user-config" | "user-management";
}>;
export const svgNames: ["dashboard", "file", "fullscreen-exit", "fullscreen", "keyboard-down", "keyboard-enter", "keyboard-esc", "keyboard-up", "search", "team-management", "user-config", "user-management"];
export type SvgName = "dashboard" | "file" | "fullscreen-exit" | "fullscreen" | "keyboard-down" | "keyboard-enter" | "keyboard-esc" | "keyboard-up" | "search" | "team-management" | "user-config" | "user-management";
export const svgNames: ["dashboard", "file", "fullscreen-exit", "fullscreen", "kb", "keyboard-down", "keyboard-enter", "keyboard-esc", "keyboard-up", "search", "team-management", "user-config", "user-management"];
export type SvgName = "dashboard" | "file" | "fullscreen-exit" | "fullscreen" | "kb" | "keyboard-down" | "keyboard-enter" | "keyboard-esc" | "keyboard-up" | "search" | "team-management" | "user-config" | "user-management";
export default SvgIcon;
}