build-flow.api/docs/superpowers/specs/2026-07-07-sop-test-unit-factory-design.md
xiangning 16ba77f69f feat(platform): 实现权限版本控制即时失效旧token机制
- 在JWT中添加permver权限版本号claim,用于实时权限变更检测
- 实现PermissionVersionMiddleware中间件,在认证后授权前校验权限版本
- 修改AssignPermissionsCommandHandler,角色权限变更时自增相关用户权限版本号
- 修改AssignRolesCommandHandler,用户角色变更时自增权限版本号强制重登录
- 集成Redis存储权限版本镜像,支持分布式环境下的版本同步
- 添加PermissionVersion字段到User实体,用于存储权限版本号
- 实现分布式登录失败计数与账号锁定功能,增强安全防护
- 配置文件中新增AccessTokenMinutes和RefreshTokenDays可配置项
- 更新CORS策略配置,添加127.0.0.1本地访问支持
- 完善异常处理,统一使用ErrorCode枚举进行错误码管理
- 添加SOP测试单元设计文档,规范端到端测试流程
2026-07-07 17:18:05 +08:00

354 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# SOP 测试单元工厂设计 · TDD + SDD 三驱动融合,推送飞书
- **状态**:已批准(Approved)
- **日期**:2026-07-07
- **作者**:brainstorming session
- **影响范围**:全 6 后端模块(POC 阶段聚焦 `rag`);产出物推送至飞书知识库 `7659208807054117857` 的"产品"目录
---
## 一、目标与背景
把 BuildFlow"现在的功能"拆解为**可执行、可追踪、可复制的最小 SOP 测试单元**,通过三位一体的工作流落地到飞书(文档 + 任务联动),实现端到端场景的标准化验证。
### 三驱动定义(与用户对齐)
| 驱动 | 全称 | 在本项目中的具体含义 |
|------|------|----------------------|
| **TDD-文档** | Technical Design Document | 技术设计文档:架构、接口契约、数据模型、时序图、边界条件 |
| **TDD-测试** | Test-Driven Development | 测试驱动开发:先写 xUnit 测试(红)→ 实现(绿)→ 重构 |
| **SDD** | Skill-Driven Development | 技能驱动开发:复用项目已有的 `.agents/skills/rag-coding-api` 编码规范(MediatR CQRS、测试规范、模块边界) |
### 现状(已通过代码探索确认)
| 维度 | 现状 | 与本设计的关系 |
|------|------|----------------|
| 测试体系 | 8 个 xUnit 测试项目,1472 源文件 vs 92 测试文件,FluentAssertions + EF InMemory | 成熟,但全部是单元/Handler 级,**无端到端 SOP 测试** |
| RAG 测试 | `BuildFlow.Rag.Tests` 仅 7 个文件,覆盖 sanitizer/parser/catalog/validator | **关键缺口**:无 ProcessDocumentCommandHandler、SendMessageCommandHandler、RagQueryCommandHandler、RAGStreamEndpoint 的集成测试 |
| 文档 | 仅 scripting/x6-workflow/CI-CD 三份设计文档,散落各子工程 | **无统一 PRD/SDD/SOP 体系** |
| 技能规范 | `.agents/skills/rag-coding-api`(27KB)已强制 MediatR CQRS、禁 service bridge、测试规范 | SDD 的载体已就绪,本设计直接复用 |
| 飞书 | 已有知识库 `7659208807054117857`,含"服务器配置""项目开发指南"两个根节点 | 本设计在其下新建"产品"目录承载 SOP 产出 |
---
## 二、核心决策(已与用户确认)
| # | 决策点 | 选择 | 理由 |
|---|--------|------|------|
| 1 | 功能范围 | 全部 6 后端模块 | rag/file/im/platform/workflow/scripting 全覆盖 |
| 2 | 推进策略 | POC 先行:RAG 跑通再复制 | 风险低、模式可复制、工作量可控 |
| 3 | 测试单元粒度 | 端到端场景 | 用户视角完整流程(非原子用例) |
| 4 | 飞书产出形式 | 飞书文档 + 飞书任务联动 | 文档沉淀设计,任务追踪执行 |
| 5 | RAG 场景 | A=知识库 RAG 问答全链路 + B=多轮对话流式问答 | 互补覆盖 RAG 两条主干(检索 vs 对话) |
| 6 | pgvector 隔离 | Stub 先行 + Testcontainers 集成测试补充 | POC 用 Stub 快速跑通,后续补真实召回验证 |
| 7 | 实现顺序 | 文档先行 → 测试后补 | 文档引导实现,飞书文档作为单一事实来源 |
| 8 | 模板化 | 8 段式文档骨架 + 5 任务结构 | Phase 2 可填模板复制到其余 5 模块 |
---
## 三、SOP 测试单元工厂架构
### 3.1 生产流水线
```
[SDD 规范] [TDD 设计] [TDD 实现]
rag-coding-api skill → 技术设计文档 → xUnit 测试先行
(MediatR CQRS / (架构/接口/数据模型/ (红色测试 →
测试规范) 时序/边界) 绿色实现 → 重构)
│ │ │
└──────────────────────┴───────────────────────┘
│
▼
┌─────────────────────────┐
│ 最小 SOP 测试单元 │
│ (端到端场景) │
└────────────┬────────────┘
│
┌─────────────┴─────────────┐
▼ ▼
┌─────────────────┐ ┌────────────────────┐
│ 飞书文档 │ │ 飞书任务 │
│ (SOP + 设计) │◄──────►│ (检查项可勾选) │
│ 评审/沉淀 │ deep │ 执行/追踪 │
│ │ link │ │
└─────────────────┘ └────────────────────┘
```
### 3.2 飞书知识库目录结构
```
📚 Wiki 空间 7659208807054117857
├── 📄 服务器配置 (已存在)
├── 📄 项目开发指南 (已存在)
└── 📁 产品/ ← 已创建 node_token=StjYwzzsSisql1ki2oqc5KFEnZd
├── 📄 产品概览 - BuildFlow 模块地图
└── 📁 SOP 测试单元/
├── 📁 RAG 模块/
│ ├── 📄 [SOP-RAG-01] 知识库 RAG 问答全链路
│ └── 📄 [SOP-RAG-02] 多轮对话流式问答
├── 📁 File 模块/ (Phase 2)
├── 📁 IM 模块/ (Phase 2)
├── 📁 Platform 模块/ (Phase 2)
├── 📁 Workflow 模块/ (Phase 2)
└── 📁 Scripting 模块/ (Phase 2)
```
---
## 四、SOP 测试单元标准模板
每个 SOP 测试单元(无论 RAG 还是后续模块)都遵循同一个 8 段式文档骨架 + 5 任务结构。这是 Phase 2 能快速复制的关键。
### 4.1 飞书文档骨架(8 段式)
```
# [SOP-XX] 场景名称
## 模块: rag | 场景类型: 端到端 | 状态: 草稿/评审/通过
## 1. 用户故事与价值
作为...我希望...以便...
## 2. 前置条件 (SDD 规范约束)
- 遵守 rag-coding-api skill: MediatR CQRS / 测试规范
- 依赖的外部服务及隔离方式 (Stub/Testcontainer)
## 3. 技术设计 (TDD 文档部分)
3.1 架构图 (mermaid 时序图)
3.2 涉及的 Command/Query/Handler 清单
3.3 数据模型变更 (实体/字段)
3.4 关键接口契约 (输入/输出)
## 4. SOP 执行步骤 (端到端)
Step 1...N: 每步含 API 调用 / Handler / 预期结果
## 5. 验收标准 (Definition of Done)
☐ 可勾选的检查项列表
## 6. 测试设计 (TDD 测试部分)
6.1 测试文件位置与命名
6.2 Stub/Fixture 设计
6.3 测试用例清单 (Given/When/Then)
## 7. 执行结果记录
- 测试运行命令与输出
- 覆盖率
- 已知限制
## 8. 变更日志
```
### 4.2 飞书任务结构(每个 SOP 单元 = 1 父任务 + 5 子任务)
```
父任务: [SOP-XX] 场景名称 - 端到端验证
├── ☐ T1: 编写 TDD 技术设计文档 (description 含飞书文档 deep link)
├── ☐ T2: 编写 xUnit 测试用例 (红色阶段)
├── ☐ T3: 实现/对齐 Handler 逻辑 (绿色阶段)
├── ☐ T4: 跑通端到端测试,记录结果 (链接到文档§7)
└── ☐ T5: 评审 + 标记场景状态为「通过」
```
任务描述嵌入飞书文档 deep link,实现**文档 ↔ 任务双向联动**。
---
## 五、RAG 模块 POC 场景详细设计
### 5.1 场景 A:[SOP-RAG-01] 知识库 RAG 问答全链路
**用户故事**:作为知识工作者,我上传一份文档到知识库,提问后能获得基于该文档的准确回答和引用来源。
**覆盖链路**:建 KB → 上传文档 → 触发处理流水线(提取/分块/向量化/入库)→ RAG 检索问答。
**SOP 步骤(端到端)**:
| Step | API 调用 | MediatR | 预期结果 |
|------|----------|---------|----------|
| 1 | `POST /knowledge-bases` | CreateKnowledgeBaseCommand | KB 创建,配 RetrievalMode=Hybrid/TopK=5/阈值=0.7 |
| 2 | `POST /knowledge-bases/{id}/documents` (multipart) | UploadDocumentCommand | Document 落库 Status=Pending |
| 3 | `POST /documents/{id}/process` | ProcessDocumentCommand | 4 阶段流水线:提取→分块→向量化→入库,Status=Processed |
| 4 | `POST /rag/query` | RagQueryCommandHandler | 返回 {answer, sources[]},sources 非空 |
**涉及 Handler**:CreateKnowledgeBaseCommandHandler、UploadDocumentCommandHandler、ProcessDocumentCommandHandler、RagQueryCommandHandler、IRagRetrievalService、IAIChatAgent。
**验收标准(DoD)**:
- ☐ 文档 Status 从 Pending → Processing → Processed
- ☐ DocumentChunk 数量 = 预期分块数,Content 非空
- ☐ /rag/query 返回 sources[] 非空且 DocumentId 匹配
- ☐ answer 包含 LLM 响应文本
- ☐ 全程无未捕获异常
**测试隔离策略**:
| 外部依赖 | 处理方式 |
|----------|----------|
| pgvector | **POC**:Stub `IRagRetrievalService` 返回预设 sources;**补充**:Testcontainers + 真实 PG+pgvector 集成测试 |
| LM Studio | Stub `IAIChatAgent`(RunAsync 返回固定文本)+ Stub `IEmbeddingService`(返回固定 768 维向量) |
| 对象存储 | Stub `IFileStorageClient`(复用 `StubFileStorageClient` 模式) |
| 文档提取 | Stub `IDocumentExtractorFactory` + `IDocumentExtractor`(返回固定文本) |
**测试文件**:`tests/BuildFlow.Rag.Tests/RagQAEndToEndTests.cs`
---
### 5.1.1 分块策略详解(ChunkingMode 6 种模式)
分块在 `ProcessDocumentCommand` 阶段 2,由 `ITextChunker.Chunk(extracted, mode, size, overlap, separator)` 完成(实现 `Infrastructure/AI/TextChunker.cs`,设计参考 Dify):
| 模式 | 值 | 切分逻辑 |
|------|---|---------|
| **General** | 0(默认) | 段落感知:先按 `\n\n` 切段,超长段按句子标点二切,overlap 取最后 1-2 句 |
| **Heading** | 1 | 按 Markdown H1-H6 切,维护 headingStack,生成 HeadingPath 如 "第三章 > 3.1 概述" |
| **ParentChild** | 2 | 父块=整段(上下文,最大 chunkSize),子块=段落再切(chunkSize/3,精确匹配),**父子都向量化** |
| **Separator** | 3 | 按自定义分隔符(默认 `\n\n---\n\n`)切分,超长用 General 二切 |
| **ByPage** | 4 | PDF 每页/PPT 每张一块,页过长用 General 二切,子块共享 PageNumber |
| **Token** | 5 | 按 token 估算:charBudget = chunkSize × 1.5(注释称"中文=2/英文=1"但实际用固定 1.5 系数,**存在实现差异**) |
**ParentChild 回填机制**:先 SaveChanges 拿父块 Guid → 用 parentIdMap[Index→Guid] 反查 → 写入子块 ParentChunkId(两轮 SaveChanges)。
**参数优先级**:模式 `doc.ChunkingModeOverride ?? kb.ChunkingMode`(文档级覆盖优先);ChunkSize/ChunkOverlap/Separator 全部来自 kb,默认 500/50/null。
### 5.1.2 向量化策略详解
- **接口**:`IEmbeddingService.EmbedAsync(单条)` + `EmbedBatchAsync(批量)`,实现 `Infrastructure/AI/EmbeddingService.cs`,OpenAI 兼容协议
- **批处理**:`EmbeddingBatchSize = 64`,≤64 一次调用,否则按 64 步长切片按原顺序拼接(顺序必须与输入一致)
- **向量维度 = 768**(非 384/1536):默认模型 `embeddinggemma-300m` 输出 768 维,DB 列 `vector(768)`,**换模型必须同步改迁移**
- **写入方式**:EF 忽略 Embedding 列(`DocumentChunkConfiguration.Ignore`),逐块 raw SQL `UPDATE document_chunks SET embedding='[...]'::vector WHERE "Id"=...`(N 条 SQL,无批量优化)
- **TokenCount 粗估**:`Content.Length / 4`,非真实 tokenizer
### 5.1.3 召回与重排算法详解
`IRagRetrievalService.RetrieveAsync(kb, question)` → `List<RetrievalHit>`,4 步流水线:
1. **粗召回** `RecallAsync`:SQL `LIMIT RetrievalTopK`(默认 20)
2. **阈值过滤**:`VectorScore >= SimilarityThreshold`(默认 0.3),纯 Keyword 跳过
3. **内存重排**:按 mode 计算 finalScore,`Take(ContextTopK)`(默认 5)
4. **父块回填** `EnrichWithContextAsync`(当前空壳,见差异清单)
**Vector 召回 SQL**(pgvector 余弦距离):
```sql
SELECT ..., 1 - (dc.embedding <=> '{q}'::vector) AS similarity
FROM document_chunks dc JOIN documents d ON ...
WHERE d."KnowledgeBaseId" = '{kbId}' AND dc.embedding IS NOT NULL
AND dc."ParentChunkId" IS NULL -- 只召回父块/独立块
ORDER BY dc.embedding <=> '{q}'::vector LIMIT 20
```
**Keyword 召回**:`ExtractKeywords` 分词(英文 `[a-zA-Z]+`≥2,中文 ≤4 整体/>4 做 2-4 字 N-gram),每个关键词 `CASE WHEN Content ILIKE @kwN THEN 1 ELSE 0` 相加得命中数。
**Hybrid 重排加权**:`finalScore = VectorScore × VectorWeight + keywordScore × (1-VectorWeight)`,默认 VectorWeight=0.7(70% 向量 + 30% 关键词)。
### 5.1.4 设计意图 vs 当前实现差异清单(技术债务)
代码探索发现 4 处差异,POC 用 Stub 会绕过,但 Testcontainers 集成测试必须覆盖:
| # | 差异点 | 设计意图 | 当前实现 |
|---|--------|---------|---------|
| 1 | **RerankStrategy 未读取** | 枚举 None/Keyword/LLM 可切换重排 | 重排分支依据是 RetrievalMode,该字段从未被读取,LLM 重排未实现 |
| 2 | **Hybrid 非双路融合** | 向量召回 + 关键词召回融合(RRF/加权) | Hybrid 只执行向量召回 SQL,关键词仅在内存重排加权参与,未做关键词路独立召回 |
| 3 | **父块回填空壳** | 召回子块→回填父块获完整上下文 | `EnrichWithContextAsync` 无 DB 查询,ContextContent=Content;SQL `ParentChunkId IS NULL` 召回的是父块而非子块 |
| 4 | **表名不一致(潜在 bug)** | EF 表 `b_document_chunks` | raw SQL 写 `document_chunks`(无 `b_` 前缀),可能导致向量写入失败 |
---
### 5.2 场景 B:[SOP-RAG-02] 多轮对话流式问答
**用户故事**:作为用户,我与 AI 进行多轮对话,AI 能记住上下文并以流式方式逐字返回。
**覆盖链路**:建会话 → 流式提问 → 多轮历史拼装 → SSE 协议 → 缓存一致性。
**SOP 步骤(端到端)**:
| Step | API 调用 | 处理路径 | 预期结果 |
|------|----------|----------|----------|
| 1 | `POST /chat/conversations` | CreateConversationCommand | 会话创建(不绑 KB) |
| 2 | `POST /chat/conversations/{id}/stream` | StreamMessageEndpoint(直连) | SSE token 流,event:done 返回 messageId |
| 3 | `POST /chat/conversations/{id}/stream`(第二轮) | 同上 | 历史拼装含第一轮 user+assistant |
| 4 | 校验 | IChatMessageCache ↔ DB | cache 与 DB ChatMessage 一致 |
**涉及 Handler/Endpoint**:CreateConversationCommandHandler、StreamMessageEndpoint、IAIChatAgent.RunStreamingAsync、IChatMessageCache、AiChatOutputSanitizer。
**验收标准(DoD)**:
- ☐ 两轮 SSE 均以 event:done 结束并返回 messageId
- ☐ 第二轮请求时,历史拼装包含第一轮的 user+assistant 消息
- ☐ AiChatOutputSanitizer 生效(去开场白/think 标签)
- ☐ Redis cache 与 DB 消息记录一致
- ☐ 会话可删除(DELETE 返回成功)
**测试隔离策略**:
| 外部依赖 | 处理方式 |
|----------|----------|
| LM Studio | Stub `IAIChatAgent.RunStreamingAsync`(yield 固定 token 序列) |
| Redis | Stub `IChatMessageCache`(内存字典模拟)或用真实 Redis |
**测试文件**:`tests/BuildFlow.Rag.Tests/ChatStreamEndToEndTests.cs`
---
## 六、Stub/Fixture 设计(共享测试基础设施)
复用并扩展现有 `PreviewDocumentChunksQueryHandlerTests` 的 Stub 模式,新增以下共享测试基础设施(放 `tests/BuildFlow.Rag.Tests/Stub/`):
| Stub/Fixture | 实现接口 | 用途 |
|--------------|----------|------|
| `StubFileStorageClient` | `IFileStorageClient` | 提供文件流(内存 Stream),不调真实对象存储 |
| `StubDocumentExtractor` | `IDocumentExtractor` | 返回固定提取文本 |
| `StubDocumentExtractorFactory` | `IDocumentExtractorFactory` | 按扩展名分发 Stub |
| `StubEmbeddingService` | `IEmbeddingService` | 返回固定维度向量(如 384 维) |
| `StubAIChatAgent` | `IAIChatAgent` | RunAsync 返回固定文本;RunStreamingAsync yield 固定 token |
| `StubRagRetrievalService` | `IRagRetrievalService` | 返回预设 sources 列表(避开 pgvector) |
| `StubChatMessageCache` | `IChatMessageCache` | 内存字典模拟 Redis cache |
**测试 Host 搭建模式**:参考 `BuildFlow.IM.Integration.Tests` 的 `ImServerFixture`/`ImServerCollection` 模式,搭建 `RagEndToEndFixture`(注册 Stub 到 DI 容器 + EF InMemory + WebApplicationFactory)。
---
## 七、Phase 划分与里程碑
### Phase 1:RAG POC(本次实现)
| 里程碑 | 交付物 | 验收 |
|--------|--------|------|
| M1: 飞书落地 | 产品目录 + 产品概览 + RAG 2 份 SOP 文档 + 对应飞书任务 | 文档可访问,任务可勾选 |
| M2: 测试实现 | 2 个 E2E 测试文件 + 7 个 Stub 类 + RagEndToEndFixture | `dotnet test` 全绿 |
| M3: POC 验收 | 测试输出回填飞书文档§7,场景状态标记「通过」 | 模板固化,可复制 |
### Phase 2:复制到其余 5 模块(后续)
按 file → im → platform → workflow → scripting 顺序,每模块选 1-2 个端到端场景,套用 Phase 1 固化的模板。
---
## 八、风险与缓解
| 风险 | 缓解 |
|------|------|
| pgvector 无法 InMemory 测试 | POC 用 Stub RetrievalService;补充 Testcontainers 集成测试(需 Docker) |
| SSE 流式测试断言复杂 | 用 `HttpContent.ReadAsStreamAsync` 逐行解析 SSE,参考现有 `OpenAiCompatibleChatStreamParserTests` |
| StreamMessageEndpoint 直连不经 MediatR | 测试用 WebApplicationFactory 走真实 HTTP,而非直接调 Endpoint |
| Phase 2 模块差异大 | 8 段式模板的可变部分集中在§3 技术设计,其余段落复用 |
---
## 九、未来扩展(YAGNI - 本次不做)
- Web 前端端到端 SOP(Playwright,现有体系已支持)
- Portal/Client 端 SOP
- 飞书任务自动同步测试结果(CI/CD webhook)
- SOP 执行自动化(CLI 一键跑全部场景)
---
## 附录 A:关键文件索引
| 路径 | 作用 |
|------|------|
| `build-flow.api/src/modules/rag/Application/` | Command/Query/Handler 全清单 |
| `build-flow.api/src/modules/rag/Domain/Entities/` | KnowledgeBase/Document/DocumentChunk/Conversation/ChatMessage |
| `build-flow.api/src/modules/rag/Infrastructure/AI/RagRetrievalService.cs` | 向量检索核心 |
| `build-flow.api/src/BuildFlow.Host/Endpoints/Rag/` | HTTP 路由 |
| `build-flow.api/tests/BuildFlow.Rag.Tests/` | 现有测试 + 本次新增 E2E |
| `.agents/skills/rag-coding-api/SKILL.md` | SDD 规范来源 |
| 飞书 Wiki `7659208807054117857` / 产品节点 `StjYwzzsSisql1ki2oqc5KFEnZd` | SOP 产出落点 |