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

10 KiB
Raw Blame History

知识库文件夹分层设计

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: 数据层

  1. 创建 Folder 模型和表
  2. 修改 Document 模型,增加 folder_id 外键
  3. 添加数据库迁移

Phase 2: 后端 API

  1. 实现文件夹 CRUD 接口
  2. 修改文档上传接口,支持 folder_id
  3. 修改文档列表接口,支持 folder_id 过滤
  4. 修改向量检索,支持 folder_id 范围限定
  5. 实现递归 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: 前端

  1. 创建 FolderTree 组件
  2. 改造 KnowledgeView 布局
  3. 实现文件夹右键菜单(重命名/删除)
  4. 实现新建文件夹弹窗
  5. 上传时强制选择文件夹

Phase 4: 测试

  1. 文件夹 CRUD 测试
  2. 级联删除测试(删除文件夹 + 文档)
  3. 向量检索按文件夹过滤测试
  4. 前端交互测试

6. 技术约束

  • SQLite 的递归 CTE 查询文件夹树
  • 删除文件夹时先删除子文件夹(递归),再删除文档
  • ChromaDB 的 where 过滤使用 $starts_with 做路径前缀匹配
  • 前端递归组件注意防止无限循环