# 知识库文件夹分层设计 > **Goal:** 为知识库添加文件夹分层组织功能,支持多层嵌套、CRUD 操作,支持知识大脑汇聚各类内容。 ## 1. 概念与愿景 知识库是用户的**资料中枢**,文件夹分层让知识更有序。用户可以按主题/项目/类型建立文件夹层级,如 `技术文档/Python/入门.pdf`。 知识大脑会汇聚来自知识库、待办、看板、论坛、对话的内容,形成完整的用户知识画像。文件夹是知识的入口分类,而非知识图谱的一部分。 ## 2. 数据模型 ### 2.1 Folder 表(邻接表模式) ```python class Folder(BaseModel): __tablename__ = "folders" user_id = Column(String(36), ForeignKey("users.id"), nullable=False, index=True) name = Column(String(255), nullable=False) parent_id = Column(String(36), ForeignKey("folders.id"), nullable=True) # NULL=根目录 # 注意: id, created_at, updated_at 继承自 BaseModel ``` **特点:** - 邻接表模式:通过 `parent_id` 指向父文件夹 - 根目录文件夹的 `parent_id = NULL` - 查询完整树结构使用递归 CTE - **唯一约束**:`user_id + parent_id + name` 组合唯一,防止同级重名 **验证规则:** - 文件夹名称不能为空,最大 255 字符 - 不允许包含字符:`/ \ * ? :` - 最大嵌套深度:10 层(防止 UI/性能问题) ### 2.2 Document 表变更 ```python class Document(BaseModel): # ...现有字段... folder_id = Column(String(36), ForeignKey("folders.id"), nullable=True) # 新增 ``` **约定:** - `folder_id = NULL` 表示文档在根目录(未分类) - 删除文件夹时,级联删除该文件夹及其所有子文件夹中的文档 ### 2.3 ChromaDB Metadata ```python { "document_id": "xxx", "document_title": "入门.pdf", "folder_path": "/技术文档/Python", # 完整路径,用于检索过滤 "file_type": "pdf", "chunk_index": 0, } ``` ## 3. API 接口 ### 3.1 文件夹管理 | 方法 | 路径 | 说明 | |------|------|------| | GET | `/api/folders` | 获取用户的完整文件夹树 | | POST | `/api/folders` | 创建文件夹 `{ name, parent_id? }` | | PUT | `/api/folders/{id}` | 重命名文件夹 `{ name }` | | DELETE | `/api/folders/{id}` | 删除文件夹(级联删除文档) | **GET /api/folders 响应:** ```json { "folders": [ { "id": "xxx", "name": "技术文档", "parent_id": null, "children": [ { "id": "yyy", "name": "Python", "parent_id": "xxx", "children": [] } ] } ] } ``` ### 3.2 文档管理变更 | 方法 | 路径 | 说明 | |------|------|------| | GET | `/api/documents?folder_id=` | 按文件夹查询文档 | | POST | `/api/documents` | 上传文档时指定 `folder_id` | | DELETE | `/api/documents/{id}` | 删除文档 | **POST /api/documents 请求增加可选字段:** ```json { "file": "", "folder_id": "yyy" // 可选,不传表示根目录 } ``` ### 3.3 安全与权限 **所有权验证:** - 所有文件夹操作必须验证 `folder.user_id == current_user.id` - 文档操作时验证 `document.user_id == current_user.id` - `folder_id` 参数需要验证归属,防止跨用户访问 **示例中间件:** ```python async def verify_folder_access(folder_id: str, user_id: str, db: AsyncSession): result = await db.execute( select(Folder).where(Folder.id == folder_id, Folder.user_id == user_id) ) if not result.scalar_one_or_none(): raise HTTPException(status_code=403, detail="无权访问此文件夹") ``` ### 3.4 向量检索变更 `KnowledgeService.retrieve()` 增加可选参数 `folder_id`: ```python async def retrieve( self, query: str, user_id: str, folder_id: str | None = None, # 新增 top_k: int = 5, ): # 如果指定 folder_id,构建 path 前缀过滤 folder_path = await self._get_folder_path(folder_id) where = {"folder_path": {"$starts_with": folder_path}} if folder_path else None ``` ### 3.5 ChromaDB 同步策略 **文件夹重命名/移动时的同步:** 由于 ChromaDB metadata 中存储了 `folder_path`,当文件夹路径变化时需要同步更新: ```python async def update_folder_paths(folder_id: str, old_path: str, new_path: str): """更新所有子文件夹和文档的路径""" # 1. 更新所有子文件夹的 path children = await db.execute( select(Folder).where(Folder.parent_id == folder_id) ) for child in children.scalars(): child_new_path = new_path + "/" + child.name await update_folder_paths(child.id, old_path + "/" + child.name, child_new_path) # 2. 更新该文件夹下所有文档的 ChromaDB metadata docs = await db.execute( select(Document).where(Document.folder_id == folder_id) ) for doc in docs.scalars(): collection.update( where={"document_id": doc.id}, set={"folder_path": new_path} ) ``` **删除文件夹时的清理:** ```python async def delete_folder_cascade(folder_id: str): """级联删除:先删子文件夹,再删文档,最后删自己""" # 1. 递归删除所有子文件夹 children = await db.execute( select(Folder).where(Folder.parent_id == folder_id) ) for child in children.scalars(): await delete_folder_cascade(child.id) # 2. 删除该文件夹下所有文档(从 ChromaDB 和数据库) docs = await db.execute( select(Document).where(Document.folder_id == folder_id) ) for doc in docs.scalars(): await knowledge_service.delete_from_vectorstore(user_id, doc.id) await db.delete(doc) # 3. 删除文件夹本身 folder = await db.get(Folder, folder_id) await db.delete(folder) ``` ## 4. 前端设计 ### 4.1 布局结构 ``` ┌─────────────────────────────────────────────────────────┐ │ KNOWLEDGE BASE [+新建文件夹] [+上传] │ ├──────────────┬──────────────────────────────────────────┤ │ │ │ │ 📁 技术文档 │ 搜索栏 [🔍 搜索...] [混合▼] │ │ 📁 Python │ │ │ 📄 入门 │ ┌─────────────────────────────────┐ │ │ 📄 进阶 │ │ 文档标题 │ │ │ 📁 Vue │ │ 类型 · 大小 · 状态 │ │ │ 📁 产品 │ └─────────────────────────────────┘ │ │ │ │ │ 📁 临时文件 │ ┌─────────────────────────────────┐ │ │ │ │ ... │ │ │ │ └─────────────────────────────────┘ │ └──────────────┴──────────────────────────────────────────┘ ``` ### 4.2 组件结构 ``` KnowledgeView ├── Header (标题 + 操作按钮) ├── MainLayout (flexbox: sidebar + content) │ ├── FolderTree (左侧边栏) │ │ ├── FolderItem (递归组件) │ │ │ ├── folder icon + name │ │ │ ├── children (递归) │ │ │ └── context menu (右键: 重命名/删除) │ │ └── AddFolderButton │ │ │ └── ContentArea (右侧主区域) │ ├── SearchBar │ ├── UploadZone │ ├── DocumentList │ └── SearchResults ``` ### 4.3 交互细节 | 操作 | 行为 | |------|------| | 点击文件夹 | 高亮选中,显示该文件夹下文档 | | 右键文件夹 | 弹出菜单:重命名 / 删除 | | 双击文件夹名 | 进入编辑状态 | | 新建文件夹 | 弹出输入框,默认在当前选中位置创建 | | 上传文档 | 需先选择目标文件夹,否则默认根目录 | | 搜索 | 可选限定在当前文件夹内搜索 | ### 4.4 UI 风格 保持一致的 sci-fi holographic 风格: - 主色调:青色 `#00f5d4` + 深色背景 - 文件夹图标:使用 Folder/FolderOpen 图标 - 悬停/选中状态:边框高亮 + 背景色变化 - 动画:展开/折叠动画 200ms ease ## 5. 实施步骤 ### Phase 1: 数据层 1. 创建 `Folder` 模型和表 2. 修改 `Document` 模型,增加 `folder_id` 外键 3. 添加数据库迁移 ### Phase 2: 后端 API 1. 实现文件夹 CRUD 接口 2. 修改文档上传接口,支持 `folder_id` 3. 修改文档列表接口,支持 `folder_id` 过滤 4. 修改向量检索,支持 `folder_id` 范围限定 5. 实现递归 CTE 查询文件夹树 **递归 CTE 示例(获取完整文件夹树):** ```sql WITH RECURSIVE folder_tree AS ( -- 基础查询:根文件夹 SELECT id, name, parent_id, 0 as depth FROM folders WHERE user_id = :user_id AND parent_id IS NULL UNION ALL -- 递归查询:子文件夹 SELECT f.id, f.name, f.parent_id, ft.depth + 1 FROM folders f INNER JOIN folder_tree ft ON ft.id = f.parent_id WHERE f.user_id = :user_id ) SELECT * FROM folder_tree ORDER BY depth, name; ``` ### Phase 3: 前端 1. 创建 `FolderTree` 组件 2. 改造 `KnowledgeView` 布局 3. 实现文件夹右键菜单(重命名/删除) 4. 实现新建文件夹弹窗 5. 上传时强制选择文件夹 ### Phase 4: 测试 1. 文件夹 CRUD 测试 2. 级联删除测试(删除文件夹 + 文档) 3. 向量检索按文件夹过滤测试 4. 前端交互测试 ## 6. 技术约束 - SQLite 的递归 CTE 查询文件夹树 - 删除文件夹时先删除子文件夹(递归),再删除文档 - ChromaDB 的 `where` 过滤使用 `$starts_with` 做路径前缀匹配 - 前端递归组件注意防止无限循环