Add project documentation and specs

This commit is contained in:
2026-03-21 10:13:45 +08:00
parent e76f0828b9
commit 3a7f4174ab
20 changed files with 11179 additions and 0 deletions

View File

@@ -0,0 +1,307 @@
# 知识库文件夹分层设计
> **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": "<binary>",
"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` 做路径前缀匹配
- 前端递归组件注意防止无限循环