Skip to content

feat(knowledge): 知识库文件夹上传的左侧树形目录 - #50

Draft
lyingbug wants to merge 11 commits into
mainfrom
cursor/knowledge-folder-tree-5c67
Draft

feat(knowledge): 知识库文件夹上传的左侧树形目录#50
lyingbug wants to merge 11 commits into
mainfrom
cursor/knowledge-folder-tree-5c67

Conversation

@lyingbug

@lyingbug lyingbug commented Aug 4, 2026

Copy link
Copy Markdown
Owner

背景

文件夹上传此前把相对目录塞进 file_name(例如 docs/spec/design.md),导致:

  • 文档列表把整条路径当成标题渲染;
  • 没有任何方式按文件夹浏览或筛选;
  • 上传的目录层级在界面上完全丢失;
  • 归类错了只能删掉重传。

交互设计

列表展示所选目录的内容:子目录作为条目排在前面,然后是本层文档 —— 就是文件管理器的行为。因此:

  • 没有任何「范围」开关。之前有开关是因为「选中一个目录」在一个无法展示目录的列表里语义含混,而把这种含混做成控件,等于把问题转交给用户。
  • 单独上传的文件不需要专门的行或模式:它就在顶层,和文件夹并排,本来就在那儿。
  • 模式由用户正在做的事决定:浏览态(无筛选)展示一层;筛选态(关键词/标签/类型/状态/时间任一生效)在当前目录及其子目录中搜索,结果平铺、带所属目录标签,面包屑后面标注「(含子目录)」。

目录调整都用轻量浮层,不用模态弹窗:移动文档是一个小的、可逆的选择,行菜单里它就是已经打开的那个菜单的下一层(与紧挨着的「移动到知识库」子菜单同一套交互),批量工具栏里它挂在按钮上;点中目录即执行,没有多余的确认步骤。目录本身在树上就地重命名,连带整棵子树。

两者都只是 folder_path 一列的更新,不触发重新解析、切块或向量化。

改动

后端

  • knowledges 新增 folder_path 列(Postgres 000079 / SQLite 000002 迁移),并把历史 file_name 中的目录部分回填进去,file_name 只保留基础文件名。
  • 上传时把 fileName 表单字段拆分成 folder_path + file_name,路径经过归一化(去掉 ../.、限制深度与长度)并复用现有输入校验。
  • 文档列表新增 folder_path / folder_recursive 查询参数;文件夹维度按参数是否出现生效,因为空字符串本身是有意义的值(知识库顶层)。
  • GET /api/v1/knowledge-bases/:id/knowledge/folders 返回带每层文档数的目录树。
  • POST /api/v1/knowledge/folder 批量修改归属目录;目标路径不存在时隐式创建,空路径表示顶层。
  • PUT /api/v1/knowledge-bases/:id/knowledge/folders 重命名/移动目录及其子树;目标已存在时合并,拒绝移动到自身子目录。
  • 跨知识库克隆时会带上 folder_path

前端

  • 左侧目录树 KbFolderTree.vue,风格沿用 Wiki 侧边栏的行高、缩进与 TDesign 变量;根节点是真实父节点,所有文件夹嵌在其下;hover 出现重命名入口,行内编辑。
  • FolderPickerMenu.vue:一份目录选择实现,被行菜单、列表行菜单、批量工具栏三处复用;含「新建子目录」行内输入,回车即创建并移动。
  • 卡片视图与列表视图新增子目录条目;面包屑可逐级返回。
  • 选中目录只是给现有列表加一个维度,搜索、标签、类型、状态、时间筛选与分页全部照常工作。
  • 上传目标不再由侧边栏选中项隐式决定:确认弹窗在目标不是顶层时显示一行说明并可一键改回顶层,文件列表里每项显示它自身的子目录。
  • 纯逻辑(上传路径拼接、面包屑、祖先展开、目录查找、树展平、子目录提取、浏览/筛选判定、路径归一化、移动合法性)抽到 folderTree.ts,与 kbListMerge.ts / wikiStatusRefresh.ts 的惯例一致,并配单测。
  • 四种语言的文案齐备。

修过的两个自身 bug

根目录行常驻一个重命名输入框。 重命名状态用空字符串表示「没有在重命名」,而空字符串恰好是根目录自己的路径,于是根目录行永远匹配。哨兵值改为 null,并额外要求该行是真实文件夹,两道独立的闸;KbFolderTree.test.ts 锁住这两条,避免复发。

上传目标行把中文目录名显示错乱。direction: rtl 从左侧截断会重排 CJK 文本。改为显示目标路径的最后一段、完整路径放在 title,并从灰色横幅改成一行安静的说明文字。

测试

自动化

  • go build ./...go test ./internal/types/... ./internal/handler/... ./internal/application/repository/... ./internal/application/service/... 全绿。新增 7 个测试文件覆盖路径归一化、树构建、仓储筛选/聚合、批量改目录的跨库隔离、子树前缀重写(含同前缀兄弟目录不被误伤)、上传路径拆分、handler 参数语义、移动合法性校验。
  • npm test 330 passed、npm run type-checknpm run check-i18nnpm run build

接口级端到端(Lite 模式 + SQLite,本地 embedding 桩服务)

把两个未归类文件移动到全新目录 归档/2026,目录被隐式创建、顶层直属数归零:

before: root direct=2 total=8
after:  root direct=0 total=8 / 归档 (0/2) → 归档/2026 (2/2)

重命名 团队手册/handbook团队手册/员工手册 连带子树共 4 条,onboardingpolicies 随之改路径;护栏生效:移动到自身子目录被拒(「不能将文件夹移动到它自己的子目录下」)、空目标被拒、不存在的目录为 no-op。

浏览态与筛选态逐一核对:

操作 结果
浏览顶层 0 条本层文档(子目录由列表条目呈现)
浏览 团队手册 1 条
浏览 团队手册/员工手册 1 条
浏览 归档/2026 2 条
顶层搜 standalone 2 条(跨子目录命中)
团队手册 内筛 md 6 条(整棵子树)

此外:folder_path=../../… 被归一化,无法越出知识库顶层;所有文档 parse_statuscompleted,即 folder_path 不影响解析链路。

界面手工验证由作者本地进行。

附注(不在本 PR 范围内)

在本地跑 Lite 模式时发现 migrations/sqlite 落后于 migrations/versioned,缺少 tenants.api_principal_configusers.is_system_adminmessages.attachmentsknowledges.pending_subtasks_countmcp_oauth_tokens.principal_type 以及 task_pending_ops 表,导致 Lite 版无法完成 auto-setup。这是既有问题,与本次改动无关,本 PR 未做修改。

Open in Web Open in Cursor 

ljling90 and others added 11 commits August 4, 2026 16:46
…/mismatched results

If the embedding provider returns an empty slice of vectors (e.g. length 0)
due to api mismatch or empty input, BatchEmbed will return length 0.
Accessing embedding[i] directly in BatchEmbedWithPool results in an out of bounds panic.
Because the panic occurs inside a mu.Lock() block, the mutex is never unlocked,
leading to a permanent deadlock of the document processing queue.

This fix checks the returned embeddings slice length and returns an error
instead of panicking, releasing the mutex properly.
…er tree API

Folder uploads used to encode their relative directory inside file_name,
which made the document list render the whole path as the title and left
no way to browse or query a single folder. The directory now lives in a
dedicated folder_path column (with a backfill for existing rows), the
document list accepts folder_path / folder_recursive filters, and a new
GET /knowledge-bases/:id/knowledge/folders endpoint returns the folder
tree with per-folder document counts.
The documents tab gains a left folder column built from the folder_path
now stored on each knowledge entry. Selecting a folder filters the
existing document list (so search, tags, type, status and date filters
all keep working), a breadcrumb above the list shows where you are, and
uploads made while browsing a folder land in that folder. A per-row
folder chip appears only when the list can span folders.
The upload-path builder, breadcrumb/ancestor derivation, folder lookup and
tree flattening were inline in the view and the sidebar component. Moving
them into folderTree.ts follows the convention of kbListMerge.ts and
wikiStatusRefresh.ts, removes the duplicated webkitRelativePath handling,
and makes the folder navigation logic directly testable.
The sidebar listed "all documents" and "root" as two rows sitting next to
the top-level folders, but those folders are inside the root, so the three
rows contradicted each other and reaching the top level for an upload meant
selecting a row that did not look like a parent.

There is now a single root row with every folder nested beneath it, and what
a row lists is decided by one uniform rule at every level: the direct
documents, or the whole subtree when the scope toggle is on. "Documents not
in any folder" is the root with the toggle off, which is also why the toggle
now defaults to on.

The upload destination is no longer implied by the sidebar selection either:
the confirmation dialog shows which folder the batch lands in, lets it be
switched back to the top level, and lists each file's own sub-directory.
…n the tree

A folder-only tree cannot show a node for a document that is not in any
folder, so single-file uploads were unreachable from the sidebar: a base
with 5 documents and one 3-document folder just left two of them
unaccounted for.

Rather than add a second pseudo-folder row for them (the ambiguity the
previous commit removed), the one existing mechanism is made visible: the
scope switch is now a labeled segmented control instead of an icon behind a
tooltip, and the root row is named after whatever the current scope makes it
list - the whole base, or exactly the unfiled documents. The breadcrumb's
leading crumb follows the same rule.
The scope switch is gone. It existed because "select a folder" was ambiguous
in a list that could not show folders, and turning that ambiguity into a
control just moved the problem onto the user.

The list now shows what a folder contains: its sub-folders as entries first,
then the documents directly inside it. So the mode follows what the user is
already doing instead of a switch - browsing shows one level, and any active
filter searches the whole subtree flat, with a breadcrumb note saying so. A
document uploaded on its own needs no special row either: it sits at the top
level next to the folders, exactly where it is.

Folders can also be adjusted now, which they could not before - a mis-filed
upload previously had to be deleted and re-uploaded. Documents move via the
row menu or the batch bar into an existing or newly typed folder, and a
folder can be renamed in place, carrying its subtree. Both are plain
folder_path updates: nothing is re-parsed, re-chunked or re-embedded.
…e the folder picker to a popup

Two things the folder UI got wrong.

The rename state used the empty string to mean "nothing is being renamed",
but the empty string is also the root folder's own path, so the root row
matched and permanently rendered a stray rename input. The sentinel is now
null, and rename is additionally gated on the row being a real folder, so no
sentinel value can reach the root again.

Moving documents also did not deserve a modal dialog: it is one small,
reversible choice. In a row's menu the picker is now another level of the menu
that is already open, mirroring the "move to knowledge base" sub-menu right
next to it; in the batch bar it hangs off the button. Picking a folder performs
the move, since there is nothing else to confirm.

The upload destination line was also mangling folder names: truncating from the
left with direction: rtl reorders CJK text. It now shows the destination's last
segment with the full path in the title, reads as a quiet line instead of a
grey banner, and only appears when the destination is not the top level.
…alog

- Added folder options to the upload confirmation dialog, allowing users to select from existing folders as upload destinations.
- Updated folder tree UI to improve folder browsing experience, displaying sub-folders and documents more intuitively.
- Enhanced folder picker functionality to support creating new sub-folders directly from the picker.
- Improved localization for folder-related strings across multiple languages.
- Refactored related components and tests to accommodate new features and ensure proper functionality.

This commit streamlines the user experience for managing folders and uploading documents, making it easier to navigate and organize content.
Document folder tree/move/rename endpoints in docs/api, Swagger, and the Go
client; align frontend path normalization with server caps; guard rename
no-ops and duplicate folder creation; enforce KB ownership on folder rename.
…nderscore scenarios

- Updated SQL queries to properly escape LIKE wildcards in folder path filters.
- Added tests for renaming knowledge folder paths, including cases with underscores in paths.
- Enhanced existing tests to ensure correct folder listing behavior when using underscores.
@lyingbug
lyingbug force-pushed the cursor/knowledge-folder-tree-5c67 branch from cb7db8f to e5b1f78 Compare August 4, 2026 12:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants