知识库真实目录、统一名称与导入登记
状态:implemented 类型:architecture Owner:backend/yuxi/modules/knowledge/services/imports.py
问题
本文面向知识库贡献者。路径型文件与真实 parent_id 目录混用,会让外观相同的目录具有不同的修改能力。先查后写无法保证并发名称唯一,移动也会制造重名。知识库级内容拒重阻止相同内容出现在不同目录。摄取 worker 创建正式记录,使名称、目录和任务的发布点分离,重试缺少持久登记身份。
目标是可写知识库使用真实目录树,所有有效节点共享同级名称空间,上传分配可用叶子名,重试复用登记结果。非目标是版本管理、通用文件系统、对象共享去重、迁移控制台以及改变外部只读连接器契约。
决策
实现方案
知识库 services 拥有名称、目录与导入登记;repositories 拥有调用方事务中的查询和写入;PostgreSQL 拥有最终约束。file_id 是身份,filename 是叶子名,parent_id 是位置,原件地址和来源信息独立于目录。名称按 Unicode NFC、去首尾 ASCII 空格、数据库 lower 比较;展示保留大小写。拒绝空名、点目录、分隔符及控制字符,最长 512 字符。有效文件与目录共享部分唯一索引,根目录 NULL 使用 NULLS NOT DISTINCT。失败文件继续占名,软删除释放名称。
手动新建目录、重命名、移动撞名返回 409;上传目录链复用已有目录,普通文件占用中间目录名时报冲突。上传叶子文件使用最小可用正整数后缀,保留最后扩展名并为后缀截短主名,不解析用户已有数字。目录树锁、读取和写入共享事务,唯一约束拒绝绕过应用锁的重名。
原件暂存使用独立对象身份,不按内容拒重,不共享对象。暂存对象使用固定物理叶子名,原始展示名称通过 Content-Disposition 元数据保存;目录相对路径独立传入来源清单。每次正式登记复制为 file_id 独占原件,删除一个条目不能损坏另一个条目。登记用例在事务前校验完整 manifest、对象范围、原件及名称,再在有限事务内建立目录链、分配名称、保存文件和登记处理作业,提交后投递。worker 只处理已登记 file_id。文件级失败使用 savepoint,成功条目的目录链完整;解析失败不撤销登记。
持久导入回执以操作者和导入标识唯一,绑定知识库、目标、条目和处理参数指纹,保存逐项结果及作业引用。显式 import_id 支持主动重新导入;已有调用方未提供时以稳定暂存对象清单导出标识,同一暂存清单视为同一意图。重放原登记结果,标识对应不同请求返回 409;文件或知识库删除不删除回执,旧请求不能复活文件。相同内容允许形成独立条目。
知识域 Schema 版本为 2,业务域保持版本 1;该决定部分取代版本 1 基线决定的知识域编号,初始化器仍不自动升级历史库。旧进程停止写入后执行显式离线转换。转换先只读审计,歧义目录、普通文件占用目录前缀、跨库父关系或目录环阻止转换并报告;不猜测合并意图。转换按有限事务恢复真实父关系,重复叶子文件按稳定顺序加后缀,保留文件 ID、原件、解析与索引身份。转换创建的目录带有来源标记,只用于识别中断重入,不将既有真实目录和虚拟路径猜测合并。失败或中断可重入;约束和版本只有在全部数据收敛后生效。转换期间 API/worker 不接流量,不增加常驻迁移任务或 UI。
可写知识库列表只查询真实 parent_id,移除虚拟目录投影、前端路径前缀及旧同名提示。Web、CLI、URL、工作区导入沿实际消费者收敛,既有任务 payload 在切换前审计,未完成旧任务阻止切换。外部只读连接器保持既有检索契约。
替代方案
- 只加 create/rename 查重:无法覆盖并发、移动及自动实体化,拒绝。
- 继续维护两种目录:同一界面长期存在不同能力,拒绝。
- 先登记路径文件再后台实体化:暴露不完整归属,拒绝。
- 新建对象共享、版本或策略框架:无当前必要 consumer,拒绝。
- 在线后台迁移:需要旧新写入协议并存;本次选择维护窗口及可重入离线转换。
后果
同级名称只有 PostgreSQL 一处最终约束,创建、重命名、移动和导入共用目录事务。非法输入返回 422,名称占用与导入标识冲突返回 409;单项登记失败与成功项分开返回,成功项继续处理,文件级 savepoint 不留下半条目录链。登记回执保存的是登记结果,处理终态仍从文件和作业读取。显式新 import_id 或新暂存对象表示重新导入,原请求的已知失败不会因重放而重新执行。
按知识库串行化目录结构写入,批量上限 500,来源路径最多 32 层、4096 字符。后缀候选在 PostgreSQL 集合查询中选取,避免高重名批次反复网络查询;扩展名预算耗尽只拒绝该条目。原件独占增加对象复制成本;未知提交结果保留对象以避免误删已提交原件,不承诺跨 PostgreSQL 与 MinIO 的原子事务。
历史转换要求维护窗口、完整备份及所有写入方停写;跨版本回退依赖整套备份恢复。实际存量中的歧义必须由数据 Owner 决定,转换器不代替用户决定合并。隔离旧 Schema 演练证明转换行为,不代表任何目标部署已完成迁移。
验证
真实数据库、HTTP 和 worker 在独立 Compose 项目中执行,重新读取 PostgreSQL 行、MinIO 原件字节和解析产物。名称预期使用固定输入及手写结果;唯一约束负控在隔离旧 Schema 中移除索引,确认重复写入确实可复现。CI 的真实 HTTP/worker gate 和 service integration gate 包含对应回归。
| 证据 | 结果与边界 |
|---|---|
backend/test/integration/api/test_knowledge_directory_imports.py 与相关 HTTP/worker 路由回归 | 91 passed:并发后缀、根目录/NFC/大小写、移动回滚、非法清单、逐项失败、原件独占、幂等与删除重放、真实解析产物、发布失败恢复、权限与跨库隔离 |
backend/test/integration/services/test_knowledge_directory_migration.py 与 Schema/投影/关系回归 | 49 passed:中断重入、稳定文件身份与来源映射、真实/虚拟目录歧义、文件占用目录前缀、旧活动任务阻断、约束和版本发布 |
| 后端完整 unit | 3100 passed、107 skipped;skip 不计入完成证据,核心目录语义由上述真实服务测试证明 |
| 前端检查与页面 | lint、804 项 unit(0 skipped)与 build 通过;页面验证同名文件后缀、真实多级目录上传与浏览、重名目录拒绝;页面结果与上传原件通过真实服务核对 |
| 名称分配规模边界 | PostgreSQL 单事务 500 个同名叶子登记测量 1.526 秒,仅含数据库登记,不包含对象 I/O 或性能 SLO |
工程契约、其单元测试、Ruff、文档链接与构建及差异空白检查作为交付 gate 执行。实际目标环境的存量审计、备份及离线转换未执行;部署者按部署参考完成。