diff --git a/docs/superpowers/specs/2026-07-20-buildflow-api-optimization-design.md b/docs/superpowers/specs/2026-07-20-buildflow-api-optimization-design.md new file mode 100644 index 0000000..7249389 --- /dev/null +++ b/docs/superpowers/specs/2026-07-20-buildflow-api-optimization-design.md @@ -0,0 +1,416 @@ +# 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`,而生产解析器已经依赖 `IOptionsMonitor`。开始第一批生产代码修改前,先修复这两个测试的依赖注册并运行现有全解决方案测试,建立可信的绿色基线。 + +基线修复只允许调整测试装配,不改变生产行为;若全量测试发现其他失败,先记录并判断是否由当前工作树已有改动造成,不能把红色基线带入后续批次。 + +## 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`;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 从干净测试环境完整运行一次并生成归档报告。