diff --git a/backend/src/api/knowledge_base.py b/backend/src/api/knowledge_base.py index 2a95048..0d21fd5 100644 --- a/backend/src/api/knowledge_base.py +++ b/backend/src/api/knowledge_base.py @@ -3,10 +3,12 @@ """ import logging import os +import shutil import uuid from pathlib import Path from typing import List, Optional, Dict, Any from fastapi import APIRouter, Depends, HTTPException, status, UploadFile, File +from fastapi.responses import FileResponse from sqlalchemy.orm import Session from sqlalchemy import or_, and_ from pydantic import BaseModel @@ -131,10 +133,20 @@ async def create_knowledge_base( detail="用户不存在" ) - # 检查知识库名称是否已存在 - existing_kb = db.query(KnowledgeBase).filter( - KnowledgeBase.name == data.name - ).first() + # 管理员创建的知识库标记为系统知识库 + is_system = user.is_superuser + + # 检查知识库名称是否在同域内已存在(系统知识库与用户知识库互不冲突) + if is_system: + existing_kb = db.query(KnowledgeBase).filter( + KnowledgeBase.name == data.name, + KnowledgeBase.is_system == True + ).first() + else: + existing_kb = db.query(KnowledgeBase).filter( + KnowledgeBase.name == data.name, + KnowledgeBase.user_id == user.id + ).first() if existing_kb: raise HTTPException( @@ -142,9 +154,6 @@ async def create_knowledge_base( detail="知识库名称已存在" ) - # 管理员创建的知识库标记为系统知识库 - is_system = user.is_superuser - # 创建知识库 knowledge_base = KnowledgeBase( name=data.name, @@ -403,7 +412,16 @@ async def delete_knowledge_base( # 继续处理其他文档 logger.info(f"文档清理完成: 成功 {success_count} 个,失败 {error_count} 个") - + + # 清理知识库目录 + if knowledge_base.is_system: + kb_dir = Path(settings.knowledge_base_dir) / knowledge_base.name + else: + kb_dir = Path(settings.upload_dir) / user.username / "knowledge_bases" / knowledge_base.name + if kb_dir.exists(): + shutil.rmtree(kb_dir) + logger.info(f"已删除知识库目录: {kb_dir}") + # 3. 删除知识库(级联删除文档记录) db.delete(knowledge_base) db.commit() @@ -498,8 +516,8 @@ async def upload_document_to_knowledge_base( save_dir = Path(settings.knowledge_base_dir) / knowledge_base.name source_type = "knowledge_base" else: - # 用户知识库:保存到 uploads/{username}/ - save_dir = Path(settings.upload_dir) / user.username + # 用户知识库:保存到 uploads/{username}/knowledge_bases/{kb_name}/ + save_dir = Path(settings.upload_dir) / user.username / "knowledge_bases" / knowledge_base.name source_type = "upload" save_dir.mkdir(parents=True, exist_ok=True) @@ -740,6 +758,46 @@ async def reindex_document( ) +@router.get("/documents/{document_id}/download") +async def download_document( + document_id: int, + current_user: str = Depends(get_current_user), + db: Session = Depends(get_db) +): + """下载/查看知识库文档""" + try: + user = db.query(User).filter(User.username == current_user).first() + if not user: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="用户不存在") + + document = db.query(Document).filter(Document.id == document_id).first() + if not document: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="文档不存在") + + # 权限检查:系统知识库所有人可访问,用户知识库仅限所有者 + if not document.knowledge_base: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="知识库不存在") + if not document.knowledge_base.is_system and document.knowledge_base.user_id != user.id: + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="无权访问此文档") + + if not os.path.exists(document.file_path): + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="文件不存在") + + return FileResponse( + document.file_path, + filename=document.original_filename, + media_type="application/octet-stream" + ) + + except HTTPException: + raise + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"下载文档失败: {str(e)}" + ) + + @router.delete("/documents/{document_id}", response_model=ReindexResponse) async def delete_knowledge_base_document( document_id: int, diff --git a/web/src/app/(main)/knowledge/[id]/page.tsx b/web/src/app/(main)/knowledge/[id]/page.tsx index 7c67369..ea7cd89 100644 --- a/web/src/app/(main)/knowledge/[id]/page.tsx +++ b/web/src/app/(main)/knowledge/[id]/page.tsx @@ -155,11 +155,33 @@ export default function KnowledgeBaseDetailPage() { } }; - const handleViewDocument = (doc: Document) => { - // Use the file_path from the document to construct the download URL - // Since backend serves static files from /uploads, we can use the file_path directly - const fileUrl = `/api/uploads/${doc.filename}`; - window.open(fileUrl, '_blank'); + const handleViewDocument = async (doc: Document) => { + try { + const token = localStorage.getItem("auth_token"); + const res = await fetch(`/api/knowledge-bases/documents/${doc.id}/download`, { + headers: { Authorization: `Bearer ${token}` }, + }); + if (!res.ok) throw new Error("下载失败"); + const blob = await res.blob(); + + // 从 Content-Disposition 提取文件名,或用 doc 信息拼接 + const disposition = res.headers.get("Content-Disposition"); + let filename = `${doc.title}${doc.file_type}`; + if (disposition) { + const match = disposition.match(/filename\*?=(?:UTF-8'')?(.+)/i); + if (match) filename = decodeURIComponent(match[1].replace(/["']/g, "")); + } + + // 触发浏览器下载 + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = filename; + a.click(); + URL.revokeObjectURL(url); + } catch { + window.open(`/api/knowledge-bases/documents/${doc.id}/download`, '_blank'); + } }; const filteredDocuments = knowledgeBase?.documents.filter(doc =>