build-flow.api/docs/superpowers/specs/2026-07-20-buildflow-api-optimization-design.md
2026-07-20 08:29:10 +08:00

417 lines
19 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.

# BuildFlow API 全面优化设计
## 1. 目标
本设计用于系统性修复当前 BuildFlow API 已确认的安全、可靠性、性能和架构规范问题,并建立可重复执行的深度测试与闭环 SOP。
完成标准不是“代码能够编译”,而是同时满足以下条件:
- 已确认的高风险安全问题全部关闭。
- 工作流 Timer 和异步导入在多实例、进程重启和异常重试场景下保持可靠、幂等。
- 已确认的 N+1、无界上下文、整表内存加载和串行远程调用被消除或受到明确上限约束。
- HTTP 层、模块边界、异常错误码和文件组织符合 `rag-coding-api`。
- EF Migration 可在空库和存量库上执行,并验证升级前后数据不丢失。
- 单元测试、集成测试、并发测试、性能门禁和 HTTP/SSE 冒烟测试全部通过。
- 最终提供一个可重复运行、失败可定位、结果可归档的后端优化 SOP。
## 2. 范围与兼容性
### 2.1 范围
本次覆盖:
- `BuildFlow.Host`
- `modules/platform`
- `modules/rag`
- `modules/file`
- `modules/monitoring`
- `modules/workflow`
- `shared/BuildFlow.Shared`
- 对应测试项目、EF Migration 和 SOP 脚本
### 2.2 兼容性约束
- 保持现有 HTTP 路由不变。
- 保持现有成功响应信封和 SSE 事件协议兼容。
- 权限收紧属于预期行为变化:越权、匿名 Obsidian 同步和缺少部署密钥的管理员初始化将被拒绝。
- 数据库允许新增表、字段和索引,但不得通过删除重建现有表实现迁移。
- 保留当前工作树中的用户改动;每批修改只触碰该批明确列出的文件。
- 不引入跨模块 service/interface bridge;跨模块协作只能使用目标模块 Application 拥有的 MediatR Command/Query。
## 3. 实施策略
采用“风险优先的纵向批次”,而不是按模块逐个重构或一次性大改。
本文是总设计和顺序约束,不直接展开为一个超大实施计划。五个批次分别生成独立的 TDD 实施计划;每个批次完成并通过验收后,才为下一批编写计划。这样可以让每批保持可审查、可验证和可回滚。
每个批次必须形成独立闭环:
1. 写失败测试并确认失败原因正确。
2. 实现最小修复。
3. 运行该批测试和受影响模块测试。
4. 运行架构测试和解决方案构建。
5. 记录契约、Migration、性能和未验证项。
6. 通过门禁后再进入下一批。
批次顺序固定为:
1. 安全与对象权限。
2. 持久化任务和多实例可靠性。
3. 数据库查询与批处理性能。
4. 聊天、文件和 Excel I/O 性能。
5. 架构规范收口与完整 SOP。
### 3.1 基线修复门禁
当前工作树构建通过,但 RAG 测试存在两个已知失败:测试容器注册 `IOptions<AiOptions>`,而生产解析器已经依赖 `IOptionsMonitor<AiOptions>`。开始第一批生产代码修改前,先修复这两个测试的依赖注册并运行现有全解决方案测试,建立可信的绿色基线。
基线修复只允许调整测试装配,不改变生产行为;若全量测试发现其他失败,先记录并判断是否由当前工作树已有改动造成,不能把红色基线带入后续批次。
## 4. 第一批:安全与对象权限
### 4.1 管理员初始化
现状会在空库中创建固定密码 `admin123` 的最高权限管理员。
目标设计:
- 新增 `Platform:BootstrapAdmin` 配置节,包含 `Enabled`、`Username`、`Email` 和 `Password`。
- 默认 `Enabled=false`,生产环境不会隐式创建管理员。
- 启用初始化时,密码必须来自 IConfiguration,即环境变量、Secrets 或 Consul;仓库配置文件不得提供真实默认密码。
- 密码必须通过与注册接口相同的强度规则。
- 数据库已有管理员时不重置密码。
- 日志只能记录“已创建/已跳过”,不得记录账号密码。
验收证据:
- 未启用时空库不会创建固定管理员。
- 启用但密码缺失或弱密码时启动种子步骤明确失败并记录原因。
- 合法部署密钥只在首次初始化创建管理员,后续启动不修改密码。
- 生产源码和配置扫描不存在固定管理员默认口令;测试样例和安全设计文档不计入该扫描。
### 4.2 Obsidian 同步边界
目标设计:
- Endpoint 从 `AllowAnonymous()` 改为现有知识库写权限。
- Handler 注入 `ICurrentUserContext`,验证知识库属于当前用户;知识库以 `CreatedBy` 作为现有所有权依据。
- 新增 `Rag:Obsidian` 配置:`AllowedRoots`、`MaxFiles`、`MaxFileBytes`、`MaxTotalBytes`。
- 请求路径规范化后必须位于某个允许根目录中;符号链接解析后不得逃逸根目录。
- 枚举过程流式执行,不先对整个目录 `ToList()`。
- 在读取前检查文件数、单文件大小和累计大小,超限抛 `ErrorCode.TooLarge`。
- 单文件读取使用流式哈希,不为计算 SHA256 分配完整 `byte[]`。
- 目录权限、文件消失和取消操作必须产生可诊断日志,不泄露文件内容。
验收证据:
- 未认证、无权限、非所有者、根目录逃逸、符号链接逃逸、数量超限和大小超限均被拒绝。
- 合法目录可增量同步,未变文件不会重复写入。
- 大目录测试证明内存不随文件总大小线性增长。
### 4.3 RAG 会话所有权
以下操作统一执行对象级所有权校验:详情、更新、删除、普通发送、流式发送、编辑后重新生成。
目标设计:
- Application handler 注入 `ICurrentUserContext`。
- 查询条件直接包含 `Conversation.Id` 和 `Conversation.UserId`,避免先查再判断产生信息泄露。
- 非所有者统一抛 `NotFoundException(ErrorCode.NotFound, "会话不存在")`,不暴露资源是否存在。
- Endpoint 不承载所有权逻辑。
- 相关缓存 key 仍以会话 ID 标识,但只有通过所有权校验后才能访问。
验收证据:
- 用户 A 无法读取、更新、删除或向用户 B 的会话发送消息。
- 用户 A 的所有现有会话操作保持兼容。
- 流式和非流式路径使用同一所有权规则。
### 4.4 配置日志
- Consul Provider 不再输出任何配置值或其截断片段。
- 日志只包含 key 数量、版本索引和刷新结果。
- 增加静态测试,禁止配置 Provider 出现值输出模板。
## 5. 第二批:持久化任务和多实例可靠性
### 5.1 工作流 Timer 数据模型
新增 `WorkflowTimerJob`,映射到 `b_workflow_timer_jobs`。核心字段:
- `Id`
- `InstanceId`
- `TokenId`
- `NodeId`
- `DueAt`
- `Status`
- `LeaseOwner`
- `LeaseExpiresAt`
- `AttemptCount`
- `LastError`
- `CompletedAt`
- 标准审计字段
约束与索引:
- `TokenId` 唯一,确保一个 token 最多一个 Timer Job。
- `(Status, DueAt)` 组合索引支撑到期扫描。
- `(Status, LeaseExpiresAt)` 支撑过期租约恢复。
状态限定为 `Pending`、`Processing`、`Completed`、`Failed`、`Cancelled`。
### 5.2 Timer 执行链
- `ProcessEngine` 遇到未来 Timer 时只持久化 Timer Job,不再创建 `Task.Run`。
- 新增 Host `WorkflowTimerSchedulerService`,只负责周期触发 Application processor。
- Application processor 使用 PostgreSQL `FOR UPDATE SKIP LOCKED` 或等价原子语句批量抢占到期任务。
- 抢占时写入实例唯一 `LeaseOwner` 和 `LeaseExpiresAt`。
- 处理前重新验证实例、token 和 Timer Job 状态。
- 消费 token、创建下游 token、更新 Timer Job 必须处于同一数据库事务。
- 进程中断后,租约过期的 `Processing` 任务重新进入可领取状态。
- 达到最大重试次数后标记 `Failed` 并保留 `LastError`,不得无限热重试。
- 实例终止或 token 非 Active 时标记 `Cancelled`,不得继续投递。
### 5.3 导入任务原子抢占
在现有导入任务增加:
- `LeaseOwner`
- `LeaseExpiresAt`
- `AttemptCount`
- `LastHeartbeatAt`
并增加 `(Status, CreatedAt)`、`(Status, LeaseExpiresAt)` 索引。
后台服务不再先查询 ID 再普通更新,而是调用 Application 层原子领取命令。领取规则:
- 只领取 `Pending` 或租约过期的 `Running` 任务。
- 一次最多领取配置数量。
- 多实例不得领取同一任务。
- 处理过程中按批次更新心跳。
- 完成、失败和取消都清理租约字段。
验收证据:
- 两个并发 worker 只能有一个获得同一 Timer 或导入任务。
- worker 在领取后崩溃,租约过期后另一 worker 能恢复任务。
- 相同 token 不会创建两个下游 token。
- 导入恢复不会重复插入已经确认完成的批次。
## 6. 第三批:数据库查询与批处理性能
### 6.1 表单统计
把当前“目标行 × 统计字段”的查询模式改为一次构建计算计划:
- Schema 只解析一次。
- 按来源表单编码分组统计字段。
- 一次批量加载来源表单定义。
- 一次批量加载当前实例需要的来源 FormData。
- 在内存中按来源表单和关系值建立索引。
- 计算全部目标行后统一更新 `WorkflowInstance.Variables` 一次。
- 单次刷新只执行一次 `SaveChangesAsync`。
查询次数门禁不依赖运行时长,而使用 EF CommandInterceptor 计数。固定输入规模增大时,SQL 次数必须保持常数级,不得随目标行数和统计字段数线性增长。
### 6.2 导入批处理
- Excel 解析结果以流或可迭代行提供,不一次性保存全部行对象。
- 每批默认 200 行,可配置但有上限。
- 每行继续执行派生和 Schema 校验,但表单定义与 Schema 解析结果在任务内复用。
- 每批执行一次数据库提交和一次任务进度/心跳更新。
- 聚合字段在批次写入后统一刷新,不为每行单独刷新两次。
- 移除手动 `new SubmitFormDataCommandHandler(...)`,由模块内明确的批量用例承载导入逻辑。
- 行 InstanceId 使用完整 `jobId + rowIndex` 确定性哈希,不再只覆盖 Guid 的两个字节。
- 每个导入任务记录已提交行游标,使租约恢复从下一未完成批次继续。
### 6.3 Monitoring UPSERT
- `SyncedRecord` 以现有 `(ExternalSystemId, ExternalId, RecordType)` 唯一键进行 PostgreSQL `ON CONFLICT DO UPDATE`。
- 每批最多 500 条,参数化执行,不拼接外部输入。
- 同步任务、记录写入和系统最近同步信息保持清晰事务边界。
- JSON payload 解析失败仍保留 `_raw`,但记录结构化告警。
验收证据:
- 同步 N 条记录不再产生 N 次查询。
- 并发同步相同外部记录不产生重复行或唯一键异常。
- 更新记录保留最新 `SyncJobId`、`OccurredAt` 和 payload。
## 7. 第四批:聊天、文件和 Excel I/O 性能
### 7.1 聊天上下文窗口
新增 `Rag:Chat` 配置:
- `MaxContextMessages`
- `MaxContextTokens`
- `CacheTtlMinutes`
生成路径按 `ConversationId + CreatedAt` 倒序只读取有限消息,再恢复时间顺序。`ChatContextWindowBuilder` 从最新消息向前累加估算 token,达到上限后停止;完整历史仍保留在数据库。
新增 `(ConversationId, CreatedAt)` 组合索引。普通发送、流式发送和编辑后重新生成必须复用同一个上下文构建器,避免三套行为漂移。
### 7.2 Redis 消息缓存
- key 固定为 `chat:messages:{conversationId}`。
- 存储结构从单个 JSON 字符串改为 Redis List。
- 追加使用原子 `RPUSH`,并通过事务或 Lua 同时刷新 TTL。
- 读取详情使用 `LRANGE`,缓存 miss 才回源数据库并重建 List。
- 删除会话继续删除整个 key。
- 不再使用非原子“读完整列表、追加、写完整列表”。
验收证据:
- 多个并发追加不会丢消息。
- 单次追加传输量不随历史长度增长。
- 缓存失效后可从数据库完整重建。
### 7.3 文件大小回填
- 新增 `SizeVerified` 字段;新上传文件在落库时直接标记已验证。
- 历史记录迁移规则:`Size > 0` 标记已验证,`Size == 0` 保持待验证。
- 文件夹树和文件列表不再调用 S3 HeadObject,也不在 GET 请求中写数据库。
- 新增后台修复 processor,每轮领取有限数量未验证文件,并以配置的有限并发查询 S3。
- 对象大小为 0 仍标记已验证;对象不存在记录警告并标记已验证,避免永久重试。
- 临时网络失败保持未验证,下一轮重试。
### 7.4 Excel 边界
- 上传入口在读取前限制文件字节数。
- 导入限制最大数据行数和最大单元格文本长度。
- 导入使用流式 OpenXML reader,避免 `ReadAllBytesAsync` 和整表对象常驻。
- 导出使用 OpenXmlWriter 顺序写行,并从数据库按稳定游标分页读取。
- HTTP 下载保持现有文件响应契约;生成过程使用临时文件或可回收流,避免同时保留完整实体集合和最终字节数组。
- 临时文件在成功、失败和取消路径都必须清理。
验收证据:
- 超限文件在进入深度解析前被拒绝并返回 `ErrorCode.TooLarge`。
- 大数据导入/导出测试记录峰值内存,数据规模扩大时不再近似多份完整文件线性叠加。
- 取消导出后无残留临时文件。
## 8. 第五批:架构规范收口
### 8.1 Endpoint CQRS
以下 Endpoint 迁移为纯 HTTP 适配器:
- `ChangePasswordEndpoint`
- `RAGStreamEndpoint`
- `StreamMessageEndpoint`
- `EditMessageStreamEndpoint`
非流式操作使用普通 MediatR Command/Query。流式操作使用 MediatR `IStreamRequest<TEvent>`;Application handler 负责业务、持久化和产生领域流事件,Endpoint 只负责把事件编码为现有 SSE 协议。
### 8.2 移除跨模块 Bridge
移除以下 Host 注册和实现:
- `InProcessUserInfoService`
- `InProcessFileStorageClient`
- `InProcessAvatarUrlResolver`
替代方式:
- IM 需要用户信息时直接发送 Platform Application 的批量用户 Query。
- RAG 需要上传/下载文件时直接发送 File Application 的 Command/Query。
- Platform 需要头像访问地址时发送 File Application 的预览路径 Query,或只返回 object key 由已有文件 Endpoint 解析;不得再注入 Host bridge。
目标模块拥有 Command、Query 和 DTO,调用模块只引用这些契约。
### 8.3 异常和文件组织
- 全量清理共享 `BusinessException`、`NotFoundException`、`UnauthorizedException` 的旧单参数调用。
- 按语义选择 `ErrorCode`,不做无脑统一替换。
- 拆分所有一个文件包含多个顶级 class、record 或 enum 的源文件。
- Command、Query、Handler、Endpoint、Request 和 DTO 继续保持一文件一类型。
### 8.4 架构护栏
扩展 `BuildFlow.Architecture.Tests`,至少验证:
- Endpoint 构造函数不允许依赖任何 DbContext。
- Host 不允许存在 `InProcess*` 跨模块桥接注册。
- 业务异常调用必须显式携带 `ErrorCode`。
- 一个源文件只允许一个顶级 class、record 或 enum,明确的嵌套配置类型除外。
- 模块不能出现独立 Program、launchSettings 或 Api 项目。
架构测试必须扫描当前源码而不是只验证少量已知文件,确保新增代码也受约束。
## 9. Migration 设计
预计涉及:
- Workflow:新增 Timer Job 表、导入租约/进度字段及索引。
- RAG:新增聊天消息组合索引。
- File:新增 `SizeVerified` 字段及待验证查询索引。
Migration 验证分两条路径:
1. 空库从零执行全部 Migration,验证最终模型与 Snapshot 一致。
2. 从本次变更前的数据库结构升级,验证原有数据、主键、外键和索引保留。
每个 Migration 必须检查生成 SQL,不接受意外 `DropTable`、`DropColumn` 或数据丢失式重建。涉及数据回填时使用明确、幂等的 SQL。
## 10. 深度测试策略
### 10.1 测试层级
- 单元测试:所有权、路径边界、上下文窗口、状态转换、批量计算和错误分类。
- EF 集成测试:使用真实 PostgreSQL 验证 Migration、锁、`SKIP LOCKED`、UPSERT 和索引支持的查询。
- Redis 集成测试:验证原子追加、并发一致性、TTL 和缓存重建。
- S3 集成测试:验证空对象、缺失对象、网络失败和后台回填。
- Host 集成测试:验证权限、响应信封、HTTP 状态、SSE 事件顺序和取消传播。
- 并发测试:至少两个 processor 实例同时竞争 Timer、Import 和 Monitoring 数据。
- 大数据测试:表单统计、导入、导出、长会话和大量待回填文件。
### 10.2 稳定性能门禁
性能验收优先使用确定性指标,避免只依赖容易抖动的墙钟时间:
- SQL 命令数量。
- 远程 S3 调用数量和最大并发数。
- Redis 每次追加的命令数量与传输对象大小。
- LLM 上下文消息数和估算 token 上限。
- 导入每批事务数量。
- 峰值托管内存和临时文件清理结果。
墙钟耗时只作为辅助报告,不作为唯一门禁。
## 11. 闭环 SOP
新增后端优化 SOP,按以下顺序执行:
1. 环境检查:.NET 10、Docker、PostgreSQL、Redis、S3 兼容服务和必要配置。
2. 静态扫描:固定密码、裸异常、Endpoint DbContext、InProcess bridge、单文件多类型、`DateTime.Now`。
3. 格式与构建:`dotnet build BuildFlow.slnx --no-restore`,要求 0 warning / 0 error。
4. 单元与架构测试:运行全部非外部依赖测试,禁止 skipped。
5. 基础设施启动:启动隔离的 PostgreSQL、Redis 和 S3 测试容器。
6. Migration 测试:空库迁移和存量结构升级。
7. 集成测试:数据库锁、UPSERT、Redis、S3、Host HTTP/SSE。
8. 并发与恢复:双 worker 竞争、worker 崩溃、租约过期、Host 重启。
9. 大数据场景:长会话、大表单、大 Excel、大目录。
10. 结果归档:命令、通过数、失败数、skipped 数、耗时、查询计数、内存结果和失败日志路径。
SOP 脚本任何一步失败必须立即返回非零退出码,不得继续输出“完成”。脚本不得修改开发者现有数据库或覆盖本地 Secrets。
## 12. 回滚与故障处理
- 每批独立提交;下一批开始前上一批必须全绿。
- 行为改造保留配置开关时,开关只用于短期回滚,不长期维持两套实现。
- Timer 和 Import Migration 的 Down 只删除本批新增结构;执行 Down 前必须明确会丢失尚未完成的任务状态。
- 后台 processor 出现持续失败时停止领取新任务,但 Host 其他 API 保持可用。
- 配置缺失导致安全边界无法建立时采用 fail closed,不回退匿名或固定密码行为。
## 13. 完成审计
最终完成前逐项核对:
- 第一至第五批所有验收证据均有对应测试或命令输出。
- 全解决方案测试通过,0 skipped;若第三方环境导致无法执行,目标不得标记完成。
- 空库和升级 Migration 均通过。
- 所有静态规范扫描为零违规。
- 现有 API 路由和 SSE 协议回归通过。
- 没有遗留 `Task.Run` Timer、非原子 Redis 读改写、导入任务先查后抢、逐记录 Monitoring 查询或 GET 路径 S3 回填。
- SOP 从干净测试环境完整运行一次并生成归档报告。