Files
JARVIS/docs/superpowers/specs/2026-03-21-knowledge-folder-design.md

308 lines
10 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 知识库文件夹分层设计
> **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` 做路径前缀匹配
- 前端递归组件注意防止无限循环