# 知识库文件夹分层设计

> **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` 做路径前缀匹配
- 前端递归组件注意防止无限循环
