feat(knowledgebase): 添加知识库头像功能

- 在知识库详情中增加头像字段
- 实现知识库头像的上传和显示功能
- 优化知识库编辑界面,支持头像修改
- 调整后端 API 和数据库以支持头像存储
This commit is contained in:
zstar 2025-06-13 18:28:49 +08:00
parent bee2ed1625
commit 86256b3399
5 changed files with 245 additions and 127 deletions

View File

@ -1,19 +1,22 @@
import traceback
from flask import request
from services.knowledgebases.service import KnowledgebaseService
from utils import success_response, error_response
from utils import error_response, success_response
from .. import knowledgebase_bp
@knowledgebase_bp.route('', methods=['GET'])
@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', ''),
'sort_by': request.args.get("sort_by", "create_time"),
'sort_order': request.args.get("sort_order", "desc")
"page": int(request.args.get("currentPage", 1)),
"size": int(request.args.get("size", 10)),
"name": request.args.get("name", ""),
"sort_by": request.args.get("sort_by", "create_time"),
"sort_order": request.args.get("sort_order", "desc"),
}
result = KnowledgebaseService.get_knowledgebase_list(**params)
return success_response(result)
@ -22,87 +25,84 @@ def get_knowledgebase_list():
except Exception as e:
return error_response(str(e))
@knowledgebase_bp.route('/<string:kb_id>', methods=['GET'])
@knowledgebase_bp.route("/<string:kb_id>", methods=["GET"])
def get_knowledgebase_detail(kb_id):
"""获取知识库详情"""
try:
knowledgebase = KnowledgebaseService.get_knowledgebase_detail(
kb_id=kb_id
)
knowledgebase = KnowledgebaseService.get_knowledgebase_detail(kb_id=kb_id)
if not knowledgebase:
return error_response('知识库不存在', code=404)
return error_response("知识库不存在", code=404)
return success_response(knowledgebase)
except Exception as e:
return error_response(str(e))
@knowledgebase_bp.route('', methods=['POST'])
@knowledgebase_bp.route("", methods=["POST"])
def create_knowledgebase():
"""创建知识库"""
try:
data = request.json
if not data.get('name'):
return error_response('知识库名称不能为空', code=400)
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'])
@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
)
kb = KnowledgebaseService.update_knowledgebase(kb_id=kb_id, **data)
if not kb:
return error_response('知识库不存在', code=404)
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'])
@knowledgebase_bp.route("/<string:kb_id>", methods=["DELETE"])
def delete_knowledgebase(kb_id):
"""删除知识库"""
try:
result = KnowledgebaseService.delete_knowledgebase(
kb_id=kb_id
)
result = KnowledgebaseService.delete_knowledgebase(kb_id=kb_id)
if not result:
return error_response('知识库不存在', code=404)
return success_response(message='删除成功')
return error_response("知识库不存在", code=404)
return success_response(message="删除成功")
except Exception as e:
return error_response(str(e))
@knowledgebase_bp.route('/batch', methods=['DELETE'])
@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} 个知识库')
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'])
@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', ''),
'sort_by': request.args.get("sort_by", "create_time"),
'sort_order': request.args.get("sort_order", "desc")
"kb_id": kb_id,
"page": int(request.args.get("currentPage", 1)),
"size": int(request.args.get("size", 10)),
"name": request.args.get("name", ""),
"sort_by": request.args.get("sort_by", "create_time"),
"sort_order": request.args.get("sort_order", "desc"),
}
result = KnowledgebaseService.get_knowledgebase_documents(**params)
return success_response(result)
@ -111,7 +111,8 @@ def get_knowledgebase_documents(kb_id):
except Exception as e:
return error_response(str(e))
@knowledgebase_bp.route('/<string:kb_id>/documents', methods=['POST'])
@knowledgebase_bp.route("/<string:kb_id>/documents", methods=["POST"])
def add_documents_to_knowledgebase(kb_id):
"""添加文档到知识库"""
try:
@ -119,89 +120,86 @@ def add_documents_to_knowledgebase(kb_id):
data = request.json
if not data:
print("[ERROR] 请求数据为空")
return error_response('请求数据不能为空', code=400)
file_ids = data.get('file_ids', [])
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
)
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
)
return success_response(data=result, message="添加成功", code=201)
except Exception as service_error:
print(f"[ERROR] 服务层错误详情: {str(service_error)}")
traceback.print_exc()
return error_response(str(service_error), code=500)
except Exception as e:
print(f"[ERROR] 路由层错误详情: {str(e)}")
traceback.print_exc()
return error_response(str(e), code=500)
@knowledgebase_bp.route('/documents/<string:doc_id>', methods=['DELETE', 'OPTIONS'])
@knowledgebase_bp.route("/documents/<string:doc_id>", methods=["DELETE", "OPTIONS"])
def delete_document(doc_id):
"""删除文档"""
# 处理 OPTIONS 预检请求
if request.method == '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')
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))
@knowledgebase_bp.route('/documents/<doc_id>/parse', methods=['POST'])
@knowledgebase_bp.route("/documents/<doc_id>/parse", methods=["POST"])
def parse_document(doc_id):
"""开始解析文档"""
# 处理 OPTIONS 预检请求
if request.method == 'OPTIONS':
if request.method == "OPTIONS":
response = success_response({})
# 添加 CORS 相关头
response.headers.add('Access-Control-Allow-Methods', 'POST')
response.headers.add('Access-Control-Allow-Headers', 'Content-Type,Authorization')
response.headers.add("Access-Control-Allow-Methods", "POST")
response.headers.add("Access-Control-Allow-Headers", "Content-Type,Authorization")
return response
try:
result = KnowledgebaseService.async_parse_document(doc_id)
return success_response(data=result)
except Exception as e:
return error_response(str(e), code=500)
@knowledgebase_bp.route('/documents/<doc_id>/parse/progress', methods=['GET'])
@knowledgebase_bp.route("/documents/<doc_id>/parse/progress", methods=["GET"])
def get_parse_progress(doc_id):
"""获取文档解析进度"""
# 处理 OPTIONS 预检请求
if request.method == 'OPTIONS':
if request.method == "OPTIONS":
response = success_response({})
# 添加 CORS 相关头
response.headers.add('Access-Control-Allow-Methods', 'GET')
response.headers.add('Access-Control-Allow-Headers', 'Content-Type,Authorization')
response.headers.add("Access-Control-Allow-Methods", "GET")
response.headers.add("Access-Control-Allow-Headers", "Content-Type,Authorization")
return response
try:
result = KnowledgebaseService.get_document_parse_progress(doc_id)
if isinstance(result, dict) and 'error' in result:
return error_response(result['error'], code=404)
if isinstance(result, dict) and "error" in result:
return error_response(result["error"], code=404)
return success_response(data=result)
except Exception as e:
print(f"获取解析进度失败: {str(e)}")
return error_response("解析进行中,请稍后重试", code=202)
# 获取系统 Embedding 配置路由
@knowledgebase_bp.route('/system_embedding_config', methods=['GET'])
@knowledgebase_bp.route("/system_embedding_config", methods=["GET"])
def get_system_embedding_config_route():
"""获取系统级 Embedding 配置的API端点"""
try:
@ -209,69 +207,68 @@ def get_system_embedding_config_route():
return success_response(data=config_data)
except Exception as e:
print(f"获取系统 Embedding 配置失败: {str(e)}")
return error_response(message=f"获取配置失败: {str(e)}", code=500) # 返回通用错误信息
return error_response(message=f"获取配置失败: {str(e)}", code=500) # 返回通用错误信息
# 设置系统 Embedding 配置路由
@knowledgebase_bp.route('/system_embedding_config', methods=['POST'])
@knowledgebase_bp.route("/system_embedding_config", methods=["POST"])
def set_system_embedding_config_route():
"""设置系统级 Embedding 配置的API端点"""
try:
data = request.json
if not data:
return error_response('请求数据不能为空', code=400)
return error_response("请求数据不能为空", code=400)
llm_name = data.get('llm_name', '').strip()
api_base = data.get('api_base', '').strip()
api_key = data.get('api_key', '').strip() # 允许空
llm_name = data.get("llm_name", "").strip()
api_base = data.get("api_base", "").strip()
api_key = data.get("api_key", "").strip() # 允许空
if not llm_name or not api_base:
return error_response('模型名称和 API 地址不能为空', code=400)
return error_response("模型名称和 API 地址不能为空", code=400)
# 调用服务层进行处理(包括连接测试和数据库操作)
success, message = KnowledgebaseService.set_system_embedding_config(
llm_name=llm_name,
api_base=api_base,
api_key=api_key
)
success, message = KnowledgebaseService.set_system_embedding_config(llm_name=llm_name, api_base=api_base, api_key=api_key)
if success:
return success_response(message=message)
else:
# 如果服务层返回失败(例如连接测试失败或数据库错误),将消息返回给前端
return error_response(message=message, code=400) # 使用 400 表示操作失败
return error_response(message=message, code=400) # 使用 400 表示操作失败
except Exception as e:
# 捕获路由层或未预料的服务层异常
print(f"设置系统 Embedding 配置失败: {str(e)}")
return error_response(message=f"设置配置时发生内部错误: {str(e)}", code=500)
@knowledgebase_bp.route('/documents/<doc_id>/parse', methods=['POST'])
def parse_document_async(doc_id): # 函数名改为 async 以区分
@knowledgebase_bp.route("/documents/<doc_id>/parse", methods=["POST"])
def parse_document_async(doc_id): # 函数名改为 async 以区分
"""开始异步解析单个文档"""
if request.method == 'OPTIONS':
if request.method == "OPTIONS":
response = success_response({})
response.headers.add('Access-Control-Allow-Methods', 'POST')
response.headers.add('Access-Control-Allow-Headers', 'Content-Type,Authorization')
response.headers.add("Access-Control-Allow-Methods", "POST")
response.headers.add("Access-Control-Allow-Headers", "Content-Type,Authorization")
return response
try:
result = KnowledgebaseService.parse_document(doc_id) # 调用同步版本
result = KnowledgebaseService.parse_document(doc_id) # 调用同步版本
if result.get("success"):
return success_response(data={"message": f"文档 {doc_id} 同步解析完成。", "details": result})
return success_response(data={"message": f"文档 {doc_id} 同步解析完成。", "details": result})
else:
return error_response(result.get("message", "解析失败"), code=500)
return error_response(result.get("message", "解析失败"), code=500)
except Exception as e:
return error_response(str(e), code=500)
# 启动顺序批量解析路由
@knowledgebase_bp.route('/<string:kb_id>/batch_parse_sequential/start', methods=['POST'])
@knowledgebase_bp.route("/<string:kb_id>/batch_parse_sequential/start", methods=["POST"])
def start_sequential_batch_parse_route(kb_id):
"""异步启动知识库的顺序批量解析任务"""
if request.method == 'OPTIONS':
if request.method == "OPTIONS":
response = success_response({})
response.headers.add('Access-Control-Allow-Methods', 'POST')
response.headers.add('Access-Control-Allow-Headers', 'Content-Type,Authorization')
response.headers.add("Access-Control-Allow-Methods", "POST")
response.headers.add("Access-Control-Allow-Headers", "Content-Type,Authorization")
return response
try:
@ -286,14 +283,15 @@ def start_sequential_batch_parse_route(kb_id):
traceback.print_exc()
return error_response(f"启动顺序批量解析失败: {str(e)}", code=500)
# 获取顺序批量解析进度路由
@knowledgebase_bp.route('/<string:kb_id>/batch_parse_sequential/progress', methods=['GET'])
@knowledgebase_bp.route("/<string:kb_id>/batch_parse_sequential/progress", methods=["GET"])
def get_sequential_batch_parse_progress_route(kb_id):
"""获取知识库的顺序批量解析任务进度"""
if request.method == 'OPTIONS':
if request.method == "OPTIONS":
response = success_response({})
response.headers.add('Access-Control-Allow-Methods', 'GET')
response.headers.add('Access-Control-Allow-Headers', 'Content-Type,Authorization')
response.headers.add("Access-Control-Allow-Methods", "GET")
response.headers.add("Access-Control-Allow-Headers", "Content-Type,Authorization")
return response
try:
@ -303,4 +301,4 @@ def get_sequential_batch_parse_progress_route(kb_id):
except Exception as e:
print(f"获取顺序批量解析进度路由处理失败 (KB ID: {kb_id}): {str(e)}")
traceback.print_exc()
return error_response(f"获取进度失败: {str(e)}", code=500)
return error_response(f"获取进度失败: {str(e)}", code=500)

View File

@ -8,7 +8,6 @@ import shutil
import tempfile
import time
from datetime import datetime
from io import BytesIO
from urllib.parse import urlparse
import requests
@ -61,7 +60,7 @@ def perform_parse(doc_id, doc_info, file_info, embedding_config, kb_info):
if embedding_model_name == "netease-youdao/bce-embedding-base_v1":
embedding_model_name = "BAAI/bge-m3"
embedding_api_base = embedding_config.get("api_base") if embedding_config and embedding_config.get("api_base") else "http://localhost:8000" # 默认基础 URL
embedding_api_base = embedding_config.get("api_base") if embedding_config and embedding_config.get("api_base") else "http://localhost:11434" # 默认基础 URL
# 如果 API 基础地址为空字符串,设置为硅基流动的 API 地址
if embedding_api_base == "":

View File

@ -105,7 +105,8 @@ class KnowledgebaseService:
k.description,
k.create_date,
k.update_date,
k.doc_num
k.doc_num,
k.avatar
FROM knowledgebase k
WHERE k.id = %s
"""
@ -334,6 +335,13 @@ class KnowledgebaseService:
update_fields.append("permission = %s")
params.append(data["permission"])
if "avatar" in data and data["avatar"]:
avatar_base64 = data["avatar"]
# 拼接上前缀
full_avatar_url = f"data:image/png;base64,{avatar_base64}"
update_fields.append("avatar = %s")
params.append(full_avatar_url)
# 更新时间
current_time = datetime.now()
update_date = current_time.strftime("%Y-%m-%d %H:%M:%S")

View File

@ -1,5 +1,19 @@
import { request } from "@/http/axios"
// 定义修改数据库界面中显示的信息
interface KbDetailData {
id: string
name: string
permission: string
avatar?: string
}
interface ApiDetailResponse {
data: KbDetailData
code: number
message: string
}
// 获取知识库列表
export function getKnowledgeBaseListApi(params: {
currentPage: number
@ -16,7 +30,7 @@ export function getKnowledgeBaseListApi(params: {
}
// 获取知识库详情
export function getKnowledgeBaseDetailApi(id: string) {
export function getKbDetailApi(id: string): Promise<ApiDetailResponse> {
return request({
url: `/api/v1/knowledgebases/${id}`,
method: "get"
@ -44,6 +58,7 @@ export function updateKnowledgeBaseApi(id: string, data: {
description?: string
language?: string
permission?: string
avatar?: string
}) {
return request({
url: `/api/v1/knowledgebases/${id}`,

View File

@ -1,6 +1,6 @@
<script lang="ts" setup>
import type { SequentialBatchTaskProgress } from "@@/apis/kbs/document"
import type { FormInstance } from "element-plus"
import type { FormInstance, UploadFile, UploadProps } from "element-plus"
import DocumentParseProgress from "@/layouts/components/DocumentParseProgress/index.vue"
import {
deleteDocumentApi,
@ -14,9 +14,11 @@ import {
batchDeleteKnowledgeBaseApi,
createKnowledgeBaseApi,
deleteKnowledgeBaseApi,
getKbDetailApi,
getKnowledgeBaseListApi,
getSystemEmbeddingConfigApi,
setSystemEmbeddingConfigApi
setSystemEmbeddingConfigApi,
updateKnowledgeBaseApi
} from "@@/apis/kbs/knowledgebase"
import { getTableDataApi } from "@@/apis/tables"
import { usePagination } from "@@/composables/usePagination"
@ -157,39 +159,91 @@ const editDialogVisible = ref(false)
const editForm = reactive({
id: "",
name: "",
permission: "me"
permission: "me",
avatar: ""
})
const editLoading = ref(false)
//
function handleEdit(row: KnowledgeBaseData) {
// //
// function handleEdit(row: KnowledgeBaseData) {
// editDialogVisible.value = true
// editForm.id = row.id
// editForm.name = row.name
// editForm.permission = row.permission
// }
//
async function handleEdit(row: KnowledgeBaseData) {
editDialogVisible.value = true
editForm.id = row.id
editForm.name = row.name
editForm.permission = row.permission
editLoading.value = true
try {
const { data } = await getKbDetailApi(row.id)
editForm.id = data.id
editForm.name = data.name
editForm.permission = row.permission
// base64
if (data.avatar && !data.avatar.startsWith("data:image")) {
editForm.avatar = `data:image/jpeg;base64,${data.avatar}`
} else {
editForm.avatar = data.avatar || ""
}
} catch (error: any) {
ElMessage.error(`获取知识库详情失败: ${error?.message || "未知错误"}`)
editDialogVisible.value = false //
} finally {
editLoading.value = false
}
}
//
function submitEdit() {
editLoading.value = true
// API
axios.put(`/api/v1/knowledgebases/${editForm.id}`, {
//
const payload: { permission: string, avatar?: string } = {
permission: editForm.permission
})
}
// Base64
if (editForm.avatar && editForm.avatar.startsWith("data:image")) {
payload.avatar = editForm.avatar.split(",")[1]
}
// 使 API
updateKnowledgeBaseApi(editForm.id, payload)
.then(() => {
ElMessage.success("知识库权限修改成功")
ElMessage.success("知识库信息修改成功")
editDialogVisible.value = false
//
getTableData()
getTableData() //
})
.catch((error) => {
ElMessage.error(`修改知识库权限失败: ${error?.message || "未知错误"}`)
.catch((error: any) => {
ElMessage.error(`修改知识库失败: ${error?.message || "未知错误"}`)
})
.finally(() => {
editLoading.value = false
})
}
const handleAvatarChange: UploadProps["onChange"] = (uploadFile: UploadFile) => {
if (!uploadFile.raw) return
if (!uploadFile.raw.type.includes("image")) {
ElMessage.error("请上传图片格式文件!")
return false
}
//
const isLt2M = uploadFile.raw.size / 1024 / 1024 < 2
if (!isLt2M) {
ElMessage.error("上传头像图片大小不能超过 2MB!")
return false
}
const reader = new FileReader()
reader.readAsDataURL(uploadFile.raw)
reader.onload = () => {
editForm.avatar = reader.result as string
}
}
//
const multipleSelection = ref<KnowledgeBaseData[]>([])
@ -1404,6 +1458,21 @@ const userLoading = ref(false)
<el-form-item label="知识库名称">
<span>{{ editForm.name }}</span>
</el-form-item>
<el-form-item label="知识库头像">
<el-upload
class="avatar-uploader"
action="#"
:show-file-list="false"
:on-change="handleAvatarChange"
:auto-upload="false"
accept="image/png, image/jpeg, image/gif, image/webp"
>
<img v-if="editForm.avatar" :src="editForm.avatar" class="avatar" alt="avatar">
<el-icon v-else class="avatar-uploader-icon">
<Plus />
</el-icon>
</el-upload>
</el-form-item>
<el-form-item label="权限设置">
<el-select v-model="editForm.permission" placeholder="请选择权限">
<el-option label="个人" value="me" />
@ -1697,4 +1766,33 @@ const userLoading = ref(false)
vertical-align: middle;
animation: rotating 2s linear infinite;
}
.avatar-uploader .el-upload {
border: 1px dashed var(--el-border-color);
border-radius: 50%; /* 圆形 */
cursor: pointer;
position: relative;
overflow: hidden;
transition: var(--el-transition-duration-fast);
}
.avatar-uploader .el-upload:hover {
border-color: var(--el-color-primary);
}
.avatar-uploader-icon {
font-size: 28px;
color: #8c939d;
width: 120px;
height: 120px;
text-align: center;
line-height: 120px; /* 垂直居中图标 */
}
.avatar {
width: 120px;
height: 120px;
display: block;
object-fit: cover; /* 保证图片不变形 */
}
</style>