10 KiB
10 KiB
知识库文件夹分层设计
Goal: 为知识库添加文件夹分层组织功能,支持多层嵌套、CRUD 操作,支持知识大脑汇聚各类内容。
1. 概念与愿景
知识库是用户的资料中枢,文件夹分层让知识更有序。用户可以按主题/项目/类型建立文件夹层级,如 技术文档/Python/入门.pdf。
知识大脑会汇聚来自知识库、待办、看板、论坛、对话的内容,形成完整的用户知识画像。文件夹是知识的入口分类,而非知识图谱的一部分。
2. 数据模型
2.1 Folder 表(邻接表模式)
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 表变更
class Document(BaseModel):
# ...现有字段...
folder_id = Column(String(36), ForeignKey("folders.id"), nullable=True) # 新增
约定:
folder_id = NULL表示文档在根目录(未分类)- 删除文件夹时,级联删除该文件夹及其所有子文件夹中的文档
2.3 ChromaDB Metadata
{
"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 响应:
{
"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 请求增加可选字段:
{
"file": "<binary>",
"folder_id": "yyy" // 可选,不传表示根目录
}
3.3 安全与权限
所有权验证:
- 所有文件夹操作必须验证
folder.user_id == current_user.id - 文档操作时验证
document.user_id == current_user.id folder_id参数需要验证归属,防止跨用户访问
示例中间件:
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:
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,当文件夹路径变化时需要同步更新:
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}
)
删除文件夹时的清理:
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: 数据层
- 创建
Folder模型和表 - 修改
Document模型,增加folder_id外键 - 添加数据库迁移
Phase 2: 后端 API
- 实现文件夹 CRUD 接口
- 修改文档上传接口,支持
folder_id - 修改文档列表接口,支持
folder_id过滤 - 修改向量检索,支持
folder_id范围限定 - 实现递归 CTE 查询文件夹树
递归 CTE 示例(获取完整文件夹树):
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: 前端
- 创建
FolderTree组件 - 改造
KnowledgeView布局 - 实现文件夹右键菜单(重命名/删除)
- 实现新建文件夹弹窗
- 上传时强制选择文件夹
Phase 4: 测试
- 文件夹 CRUD 测试
- 级联删除测试(删除文件夹 + 文档)
- 向量检索按文件夹过滤测试
- 前端交互测试
6. 技术约束
- SQLite 的递归 CTE 查询文件夹树
- 删除文件夹时先删除子文件夹(递归),再删除文档
- ChromaDB 的
where过滤使用$starts_with做路径前缀匹配 - 前端递归组件注意防止无限循环