6.9 KiB
Raw Permalink Blame History

SDD — Obsidian Vault 同步 · 软件设计文档

版本:v1.0 · 日期:2026-08-19 · 对应 PRD v1.0 事实源:真实代码。文档与代码冲突时以代码为准。


1. 总体架构

React AntD Pro (build-flow.admin)
  └─ src/pages/Knowledge/index.tsx
       └─ 「Obsidian 同步」按钮 → Modal(vaultDirectory + chunkingModeEnum)
            │  POST /knowledge-bases/{id}/obsidian/sync   (JWT, umi request)
            ▼
build-flow.api · BuildFlow.Host
  └─ Endpoints/Rag/Obsidian/SyncObsidianVaultEndpoint      [Permissions("knowledge:write")]
       │  IMediator.Send(SyncObsidianVaultCommand)
       ▼
modules/rag/Application/Obsidian
  └─ SyncObsidianVaultCommandHandler
       │  1. 校验知识库归属(Id + CreatedBy == 当前用户)→ NotFoundException
       │  2. 加载该 KB 已有 Obsidian 文档(VaultPath → 实体字典)
       │  3. 流式扫描 vault,逐文件 新增/更新/跳过
       │  4. SaveChangesAsync,返回 ObsidianSyncResult
       ▼
modules/rag/Infrastructure/Obsidian
  └─ ObsidianVaultScanner(安全解析 + 流式枚举 + SHA256)
       └─ ObsidianOptions(配置节 Rag:Obsidian)

2. 后端设计(现状)

2.1 分层与文件

层 文件 职责
Host Endpoints/Rag/Obsidian/SyncObsidianVaultEndpoint.cs 路由 POST /knowledge-bases/{KnowledgeBaseId}/obsidian/sync,权限 knowledge:write,路由参数 + 请求体组装 Command
Application Obsidian/Commands/SyncObsidianVaultCommand.cs (KnowledgeBaseId, VaultDirectory, ChunkingModeOverride?) : IRequest<ObsidianSyncResult>
Application Obsidian/DTOs/ObsidianSyncRequest.cs (VaultDirectory, ChunkingModeEnum:int?)
Application Obsidian/DTOs/ObsidianSyncResult.cs (KnowledgeBaseId, VaultDirectory, TotalFiles, Added, Updated, Skipped, Message)
Application Obsidian/Handlers/SyncObsidianVaultCommandHandler.cs 归属校验 + 增量对账 + 落库
Infrastructure Obsidian/ObsidianVaultScanner.cs 白名单/符号链接/容量门禁 + 流式枚举 .md + SHA256
Infrastructure Obsidian/ObsidianOptions.cs Rag:Obsidian 配置节:AllowedRoots[] / MaxFiles / MaxFileBytes / MaxTotalBytes
Infrastructure Obsidian/ObsidianVaultFile.cs (FullPath, RelativePath, FileName, FileSize, ContentHash)

2.2 增量对账算法(Handler)

  1. db.KnowledgeBases 按 Id == request.KnowledgeBaseId && CreatedBy == userId 查找,未命中抛 NotFoundException(ErrorCode.NotFound, "知识库不存在");
  2. 已有 SourceType == Obsidian 的文档以 VaultPath ?? FileName 为键建字典;
  3. 对扫描到的每个文件:
    • 已存在且 ContentHash 相同 → Skipped;
    • 已存在但哈希不同 → 更新 FilePath/FileName/FileSize/ContentHash/VaultPath,Status = Pending → Updated;
    • 不存在 → 新增 DocumentEntity(SourceType = Obsidian、ContentType = "text/markdown"、Status = Pending、带 ChunkingModeOverride)→ Added;
  4. 一次 SaveChangesAsync 提交,返回三计数 + 中文 message。

2.3 安全设计(Scanner)

  • 白名单:AllowedRoots 为空直接拒绝;请求路径必须落在某个已存在的配置根目录内;
  • 路径净化:拒绝含 .. 段;Path.GetFullPath 规范化;格式异常 → invalid_param;
  • 符号链接:ResolveLinkTarget(returnFinalTarget: true) 解析到真实路径后再次 EnsureWithin( vault 根与每个条目双重校验),逃逸 → invalid_param;
  • 容量门禁:流式计数,MaxFiles / MaxFileBytes / MaxTotalBytes 超限 → too_large;
  • 竞态容错:扫描期间文件消失仅记 Warning 并跳过;枚举/读取/元数据 IO 失败 → business;
  • 忽略目录:.obsidian .trash .git node_modules;仅 .md(大小写不敏感);
  • 流式:IAsyncEnumerable + 64KB 异步流式 SHA256,不全量加载内容;
  • 取消:每轮枚举与哈希均响应 CancellationToken。

2.4 配置(appsettings.json 现状)

"Rag": { "Obsidian": { "AllowedRoots": [], "MaxFiles": 10000, "MaxFileBytes": 10485760, "MaxTotalBytes": 536870912 } }

⚠️ AllowedRoots 默认为空 → 未配置时所有同步请求都会被拒绝(fail-closed,符合预期)。部署/本地调试必须显式配置,见 SOP。

3. 前端设计(本次新增)

  • 位置:build-flow.admin/src/pages/Knowledge/index.tsx(知识库列表)行内操作新增「Obsidian 同步」;
  • 交互:点击 → Modal 表单
    • vaultDirectory:Input,必填,placeholder 提示为「服务器上的 vault 绝对路径」;
    • chunkingModeEnum:Select,可清空,选项 = ChunkingModeEnum 0-5(General/Heading/ParentChild/Separator/ByPage/Token),不选表示用知识库默认策略;
    • 提交中按钮 loading + 禁用,防重复提交;
  • 结果反馈:成功后 Modal.success 展示 TotalFiles/Added/Updated/Skipped 与 message;失败由 requestErrorConfig 统一透出后端中文 message;
  • 请求:统一走 @umijs/max 的 request(自动注入 Authorization),禁止直接 fetch/axios;
  • 权限:按钮渲染受 knowledge:write 权限点控制(对齐 access.ts 现有模式)。

4. 数据模型(相关字段)

DocumentEntity:SourceType(Upload=0/Obsidian=1)、VaultPath(库内相对路径,如 Daily/2026-06-13.md)、ContentHash(SHA256 hex 小写)、ChunkingModeOverride、Status(同步后一律 Pending 交给处理管线)。

5. 错误处理映射

场景 异常 语义错误码 HTTP
知识库不存在/非本人 NotFoundException not_found 404
路径为空/含 ../格式非法/不在白名单/目录不存在/链接逃逸 BusinessException invalid_param 400
文件数/单文件/总量超限 BusinessException too_large 400
IO 失败/无读取权限/扫描期消失(条目级) BusinessException business 400
未登录 / 无 knowledge:write 中间件 — 401 / 403

6. 测试设计

  • 单元/集成(后端):BuildFlow.Rag.Tests 覆盖 Handler 三分支(added/updated/skipped)、归属校验;Scanner 白名单、..、符号链接逃逸、容量上限;
  • E2E:Playwright 驱动 React 端「登录 → 知识库 → Obsidian 同步 → 结果可见」,对接真实后端(详见 TDD.md)。

7. 已知缺陷与修复项

# 缺陷 处置
D1 端点校验 ChunkingModeEnum is >= 0 and <= 2,但枚举已有 0-5(Separator/ByPage/Token 会被静默丢弃为 null) 本期修复:改为 Enum.IsDefined 校验
D2 vault 中删除文件后知识库文档不联动删除(无删除对账) 本期范围外,PRD §6 已声明
D3 AllowedRoots 默认空数组,未配置时功能不可用但无启动期告警 SOP 中作为部署检查项