fix(knowledge): 知识库文件夹同级同名防重与创建后跳转进入 by szmadd · Pull Request #1058 · xerrors/Yuxi · GitHub
Skip to content

fix(knowledge): 知识库文件夹同级同名防重与创建后跳转进入 - #1058

Open
szmadd wants to merge 2 commits into
xerrors:mainfrom
szmadd:feat/knowledge-folder-duplicate-guard
Open

szmadd wants to merge 2 commits into
xerrors:mainfrom
szmadd:feat/knowledge-folder-duplicate-guard

Conversation

@szmadd

@szmadd szmadd commented Sep 22, 2026

Copy link
Copy Markdown
Contributor

变更说明

知识库同一父目录下可以创建任意多个同名文件夹:前端只校验名称非空,KnowledgeBase.create_folder / rename_folder 不查同级重名,knowledge_files 表也无组合唯一约束。单层浏览 + 面包屑的交互下,同级同名文件夹完全无法区分。另外新建文件夹后视图停留在父目录,上传默认目标仍是父目录,想上传到新建文件夹需要手动再点进去。

本 PR 包含两个提交:

  1. fix(knowledge):create_folder / rename_folder 持久化前通过 KnowledgeFileRepository.find_folder_by_name 按 kb_id + parent_id + 忽略大小写的 filename 在 is_folder=True 记录中查重,命中(重命名时排除自身)抛 FolderNameConflictError;两个端点映射为 409,detail 为 {"code": "folder_name_conflict", "message": ...} 对象。create_folder 同时补齐与 rename_folder 一致的名称规范化(去空白、拒绝空名与路径分隔符),其他 ValueError 在创建端点映射为 400(此前一律 500)。
  2. feat(web):新建文件夹成功后前端直接 openFolder 进入该文件夹;上传弹窗的默认目标 = 当前浏览目录(既有语义),因此新文件夹自动成为上传目标,不引入额外状态通道。
  • 任务类型:bug-fix(含一个配套的前端交互改进)
  • 目标与非目标:目标是同级文件夹名称唯一、创建后即可直接上传;非目标是文件夹与同名文件的冲突(文件查重沿用内容 hash 逻辑)、数据库唯一约束迁移、目录树展示形态变化。
  • substantial / trivial 判断:substantial(新增持久化前的防重 guard 与错误契约,接受了"应用层检查而非 DB 唯一约束"的取舍)。

工程主张与 Owner

  • 受影响的工程主张:
    1. 同一知识库同一父目录下文件夹名称唯一(此前可任意重复);大小写变体视为同名;重命名为自身当前名称放行;不同父目录下同名合法;历史存量同名数据不受影响(仅在再次重命名撞名时被拒绝)。
    2. 文件夹名称冲突通过 409 + {code, message} dict detail 暴露(沿用 run_busy、resume_superseded 等既有 dict detail 先例)。
    3. 新建文件夹后当前浏览上下文切换到该文件夹,上传默认目标随之切换。
  • Owner / commit point / 观察边界:查重 Owner 为 backend/package/yuxi/knowledge/base.py(create_folder / rename_folder),查询为 KnowledgeFileRepository.find_folder_by_name;commit point 为 _persist_file_meta / update_fields 的 PostgreSQL 写入;用户可观察面为 409 响应与前端提示。前端跳转 Owner 为 web/src/components/FileTable.vue 的 handleCreateFolder。
  • 决策记录:docs/develop-guides/decisions/implemented/2026-09-22-knowledge-folder-duplicate-guard.md。直接写 implemented 的原因:变更小而完整、在同一 PR 中生效,替代方案(DB 唯一约束、树形展示、额外上传目标状态通道)已评估并记录在案,无待裁决的开放问题。

验证情况

同级同名文件夹创建与重命名被拒绝

  • 失败面:查重缺失或排除自身逻辑错误,导致同名仍可创建,或正常重命名被误伤
  • 语义 Owner:KnowledgeBase.create_folder / rename_folder
  • 直接证据 / 命令:pytest test/unit/knowledge/test_folder_name_conflict.py(4 passed,覆盖撞名不落库、大小写、空名/分隔符、rename 撞名拒绝与自身放行)
  • 负向案例:去掉 guard 后 pytest.raises(FolderNameConflictError) 与 repo.persisted == [] / repo.updated == [] 断言同时变红
  • 结果:Passed

冲突错误以 409 + 契约 detail 返回,异名与跨父级不受影响

  • 失败面:错误状态码/契约形态错误,或跨父级同名被误拒
  • 语义 Owner:knowledge_router.create_folder / rename_folder 端点
  • 直接证据 / 命令:pytest test/integration/api/test_knowledge_router.py -k duplicate_sibling(真实 HTTP + PostgreSQL:同级创建重名含大小写变体 409 且 detail.code == "folder_name_conflict"、跨父级同名 200、rename 撞名 409、rename 自身与自身大小写变体 200、正常 rename 200)
  • 负向案例:同上用例中的正向断言即为 guard 的负控
  • 结果:Passed(在实现仓库的同语义代码上由真实页面验证覆盖;本 PR 分支环境未配置 TEST_USERNAME/TEST_PASSWORD,integration 用例按约定 skip,见「未验证范围」)

新建文件夹后跳转进入,上传默认目标切换到新文件夹

  • 失败面:创建后停留在父目录,或上传目标未跟随当前目录
  • 语义 Owner:FileTable.vue handleCreateFolder、web/src/stores/database.js enterFolder
  • 直接证据 / 命令:node --test test/unit/file_upload_folder.test.js(5 passed,含新增源码契约断言);真实页面验证:在运行相同代码的部署上新建文件夹后视图跳转进入该文件夹,随后上传的文件经 PostgreSQL 任务 payload 核实携带正确 parent_id 并落入新文件夹
  • 负向案例:还原为 refreshAfterMutation() 后新增源码断言变红
  • 结果:Passed

既有行为不回归

  • 失败面:防重或跳转影响既有文件管理、只读 connector 与列表语义
  • 语义 Owner:yuxi.knowledge.base 全部子类与 FileTable.vue
  • 直接证据 / 命令:后端 pytest test/unit -m "not slow" 864 passed(Windows 宿主机排除 106 个平台收集错误文件;63 个失败全部为 sandbox provisioner / docker-compose 契约 / nginx / CLI / symlink 类测试,在 upstream/main 基线上以完全相同的清单失败,属本机环境既有问题);前端 pnpm run test:unit 351/352 passed(唯一失败 chatStartScreen.test.js 在 upstream/main 基线同样失败,与本改动文件无交集);pnpm run lint:check 通过;python scripts/verify_engineering_contracts.py 通过;ruff check 通过
  • 负向案例:既有 test_dify_kb.py 只读 connector 拒绝 rename 用例保持绿色
  • 结果:Passed

简化 / 删除验收

不涉及。

独立语义 Review

由不继承开发上下文的 Reviewer 审查了完整需求、全量 diff、测试与仓库规范。结论:无阻断问题。逐项核实了根目录 parent_id IS NULL 情形、大小写忽略、rename 自身排除、异常映射顺序(FolderNameConflictError 非 ValueError 子类,except 顺序正确)、前端 openFolder 对返回 meta 字段的依赖、enterFolder 不会误入虚拟文件夹分支。两条非阻断建议已处理或记录:集成测试补充 rename 自身大小写变体用例(已补);存量同名数据的 rename 前提与并发双写风险已写入决策记录「后果」节。

未验证范围与风险

  • 集成测试 test_knowledge_router.py -k duplicate_sibling 在本 PR 分支环境 skip(未配置 TEST_USERNAME/TEST_PASSWORD);等价主张已由实现仓库的真实页面验证覆盖,建议在有凭据的环境补跑一次。
  • 真实页面验证在个性化部署上执行,涉及的本 PR 代码路径与上游一致(cherry-pick 无冲突),但部署的权限依赖与上游不同,不影响本功能语义。
  • 409 dict detail 的中文 message 到达用户可见文案依赖 fix(api): 业务错误 detail 契约化与前端受控透传,恢复精准错误提示 #1053 的前端透传契约;在 fix(api): 业务错误 detail 契约化与前端受控透传,恢复精准错误提示 #1053 合并前,冲突仍被正确拒绝,仅前端提示显示为 409 公共文案("请求冲突,请刷新后重试")。功能独立成立。
  • 无数据库唯一约束,并发同时创建同名文件夹仍可能双写(后果仅是多一个可删除目录),取舍见决策记录。

事故反馈

未达到 postmortem 门槛:同级同名文件夹不产生数据丢失或错误索引,仅是 UI 无法区分;已落地 guard 与负向测试。

界面变更

新建文件夹成功后视图跳转进入该文件夹(面包屑追加一级);同名冲突时弹出中文错误提示。截图可在需要时补充。

关联事项

相关讨论:#617(创建/上传位置的易用性,maintainer 确认的 VS Code 式当前目录语义——本 PR 沿用"当前浏览目录即操作上下文"模型,将创建与进入合并为一步)。

补充说明

兼容性:历史同名文件夹数据不受影响;create_folder 新增的名称规范化与 rename_folder 既有规则一致。无配置与迁移。

- create_folder/rename_folder 持久化前按同级目录与忽略大小写名称查重,冲突返回 409 {code: folder_name_conflict} 契约 detail,前端透传中文提示
- create_folder 补齐名称规范化(去空白、拒绝空名与路径分隔符)
- 决策记录:docs/develop-guides/decisions/implemented/2026-09-22-knowledge-folder-duplicate-guard.md(涵盖防重与创建后跳转两项决定)
创建成功后调用 openFolder 进入新文件夹,上传弹窗默认目标(= 当前浏览目录)随之切换到新文件夹。
@xerrors

xerrors commented Sep 22, 2026

Copy link
Copy Markdown
Owner

@xerrors xerrors self-assigned this Sep 27, 2026

This branch has not been deployed

No deployments
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