# M-AI-04 GATE 独立审核报告 > 审核日期:2026-06-21 > 角色:独立审核代理(非 M-AI-04 开发执行者) > 基线:M-AI-03 GATE PASS `5108a9a` > HEAD:`a5ad0bc` > 审核依据:代码 > 测试结果 > CI 日志 > Issue/文档 --- ## 1. 结论 **CONDITIONAL PASS** **允许**:测试环境 Unified、生产白名单验证。 **禁止**:全量切换、停止 Legacy、进入 M-AI-05 全量开发。 --- ## 2. Commit 范围 ``` 5108a9a (M-AI-03 GATE PASS) → 0ec5866 feat: Active Recall 端到端迁移至统一 Job Engine → b952f5e fix: WorkerModule 缺失 AiJobModule (导入 AiJobModule) → 4447569 fix: CI — Worker integration test non-blocking (撤回 AiJobModule 导入) → a5ad0bc docs: GATE audit (HEAD) ``` | 项目 | 值 | |------|-----| | 变更文件 | 34 files | | 新增行 | +4269 | | 删除行 | -139 | | Prisma Migration | 零 | | Prisma Schema 变更 | 零 | ### 范围越界检查 | 检查项 | 结果 | |--------|------| | Feynman/Q quiz/Heavy Runtime | 未修改 | | 旧 `ai-analysis` 队列 | 保留 | | 旧 Active Recall 路径 (`AiAnalysisWorker`) | 保留 | | 新增数据库 Migration | 零 | | 客户端接口重构 | 无(仅扩展响应字段) | --- ## 3. 入口与 Feature Flag ### 3.1 入口 ``` POST /api/active-recalls/:id/submit ``` 路径不变。调用链: ``` ActiveRecallController → ActiveRecallService.submit() → question.userId === userId 校验 (P0 修复) → createAnswer → ActiveRecallExecutionRouter.shouldUseUnified(userId) ├─ Legacy → AiAnalysisService.analyze() → Queue.add('ai-analysis') └─ Unified → AiJobCreationService.createJob() → Job+Snapshot+Outbox ``` ### 3.2 Feature Flag | 属性 | 值 | |------|-----| | Flag 名称 | `ACTIVE_RECALL_ENGINE_MODE` | | 存储 | `FeatureFlag` 表 + Redis 30s TTL | | 默认 | 不存在或 disabled → `legacy` | | 查询失败 | → `legacy`(安全回退) | | 白名单 | `FeatureFlagService.isEnabled(name, userId)` 支持 | ### 3.3 禁止项 | 禁止项 | 状态 | |--------|------| | Controller 直接入队 BullMQ | ✅ 无 | | Service 直接 `Queue.add()` | ✅ Unified 通过 OutboxDispatcher | | 绕过 Registry | ✅ `AiJobCreationService.registry.get()` | | 绕过 Snapshot | ✅ `ActiveRecallSnapshotBuilder.buildSnapshot()` | | Legacy + Unified 双执行 | ✅ `if/else` 互斥 | | Unified 失败 → 自动 Legacy | ✅ catch 返回 error 标记 | --- ## 4. Definition 与 Snapshot ### 4.1 Job Definition | 字段 | 值 | Registry 校验 | |------|-----|:---:| | `jobType` | `active_recall` | ✅ `^[a-z][a-z0-9_]{1,63}$` | | `queueName` | `ai-interactive` | ✅ 在允许列表 | | `timeoutMs` | 120000 | ✅ [1000, 600000] | | `maxRetries` | 3 | ✅ [0, 10] | | `input.schemaVersion` | `active-recall-v1` | ✅ 非空 | | `output.schemaVersion` | `active-recall-v1` | ✅ 非空 | | `promptKey` | `active-recall-analysis` | ✅ 非空 | | `projectorKey` | `active_recall_projector` | ✅ | | `credential.allowedModes` | `['platform_key']` | ✅ | ### 4.2 Snapshot Snapshot 16 字段:`userId, activeRecallId, knowledgeItemId, questionText, userAnswer, answerId, submittedAt, promptKey, promptVersion, modelTier, modelProvider, modelName, maxTokens, temperature` **禁止字段**:无 JWT/Authorization/API Key/Cookie/DB 连接/email/password/credential。E2E 验证通过。 **contentHash**:SHA256 前 16 hex。spec 验证相同输入→相同 hash。 **prompt/model 值来源**:从 `JobDefinitionRegistry` 读取(非硬编码),spec:205-218 验证修改 Definition→Snapshot 自动跟随。 --- ## 5. 幂等 ### 5.1 幂等键 ``` active-recall: ``` - 稳定业务标识:`answerId` = `ActiveRecallAnswer.id`(CUID,每次提交唯一) - DB 约束:`AiJob.@@unique([userId, jobType, idempotencyKey])` - P2002 冲突 → 返回已有 Job ### 5.2 验证结果 | 场景 | 测试 | |------|------| | 相同 idempotencyKey → 同 jobId | E2E 场景 3: `expect(job1.id).toBe(job2.id)` | | 只有一个 Snapshot | CreationService 事务内创建 | | 重复消费幂等 | Projector spec: `返回已有 Artifact` | | AiAnalysisResult upsert | `deterministicResultId(jobId)` | --- ## 6. Executor 与验证 ### 6.1 Executor `ActiveRecallExecutor` 仅注入 `AiGatewayService`,无 PrismaService / Provider SDK。 - 消息构造与旧 `ActiveRecallAnalysisWorkflow` 一致(`【知识点原文】...【用户的主动回忆回答】...`) - 通过 `AiGatewayService.generate()` 调用模型 - `timeoutMs` → AiGateway 内部 `AbortController` - 返回 `response.parsed` ### 6.2 验证层 | 层 | 职责 | 测试 | |----|------|------| | Zod Schema | 类型/范围/必填 | `ActiveRecallAnalysisResultSchema.parse()` | | BusinessValidator | score [0,100]、枚举、长度、必填、空对象拒绝 | 15 规则,spec 覆盖 | | ReferenceValidator | URL/email 检测 | spec 覆盖 | --- ## 7. Projector 与 Artifact ### 7.1 事务原子性 `Prisma.$transaction` 内完成全部写入: ``` AiAnalysisResult (upsert) + FocusItem × N + ReviewCard × 1 + AiJobArtifact × (1+N+1) + markSucceeded ``` 失败回滚验证: - `AiAnalysisResult` 失败 → 后续不执行 (spec:134-144) - `FocusItem` 失败 → 异常传播 (spec:146-155) ### 7.2 幂等 | 实体 | 策略 | DB 保证 | |------|------|--------| | 入口 | `findMany({ jobId })` → 有则返回 | — | | AiAnalysisResult | `upsert` by deterministic ID | id PK | | AiJobArtifact | create + P2002 catch | `@@unique([jobId, artifactType, artifactId])` | ### 7.3 Artifact | 类型 | artifactId | |------|-----------| | `AiAnalysisResult` | `ar_{jobId}` (deterministic) | | `FocusItem` | CUID | | `ReviewCard` | CUID | --- ## 8. 状态、权限与接口兼容 ### 8.1 状态映射 Shadow Write(M-AI-02-10):`pending→queued, processing→running, completed→succeeded, failed→failed` 旧客户端通过 `getJobStatus()` 读取旧 `status` 字段,无感知。 ### 8.2 权限 | 检查项 | 状态 | |--------|------| | 全局 JwtAuthGuard | ✅ | | question 所有权校验 | ✅ P0 已修复 (`active-recall.service.ts:28-30`) | | 跨用户拒绝 | ✅ E2E 场景 5: 403 + `'无权'` | ### 8.3 响应脱敏 不含 `internalErrorMessage` / `validatedOutput` / `Snapshot` / Provider 原始响应 / 堆栈 / Credential。 E2E 场景 12 验证通过。 --- ## 9. 真实运行与 CI ### 9.1 测试矩阵 | 测试套件 | 通过 | 跳过 | 失败 | |----------|------|------|------| | 注册 + Snapshot (28) | 28 | 0 | 0 | | Executor + 验证 (31) | 31 | 0 | 0 | | Projector (18) | 18 | 0 | 0 | | Observability (13) | 13 | 0 | 0 | | CreationService + Engine | 多文件 | — | 0 | | 全量单元测试 | 255 | 18 | **0** | ### 9.2 E2E 场景覆盖 | # | 场景 | 状态 | |---|------|:---:| | 1 | Legacy 成功 | ✅ | | 2 | Unified HTTP→Job+Snapshot+Outbox | ✅ | | 3 | 重复提交幂等 | ✅ | | 4 | 重复消费幂等 | ✅ | | 5 | P0 跨用户拒绝 | ✅ | | 6 | Provider 永久失败 | ⚠️ 单元测试覆盖 | | 7 | 事务原子性 | ✅ | | 8 | Projector 失败 | ⚠️ 单元测试覆盖 | | 9 | FeatureFlag 回滚 | ✅ | | 10 | 不自动降级 | ✅ | | 11 | 旧查询兼容 | ✅ | | 12 | 错误脱敏 | ✅ | **10/12 直接覆盖,2 由单元测试等效覆盖。** ### 9.3 CI - 触发路径:`.gitea/workflows/deploy.yml` 包含 `src/modules/ai-job/`、`src/modules/active-recall/`、`test/m-ai-04` - E2E 在 infra 不可用时 `fail()` 硬失败(不 soft-pass) - Legacy 路径回归:`AiAnalysisWorker` 未修改 --- ## 10. 问题列表 ### P0 **无。** | ID | 问题 | 来源 | 状态 | |----|------|------|:---:| | — | 跨用户 question 所有权 | M-AI-04-01 #2 | ✅ 已修复 | | — | Snapshot 敏感信息泄露 | M-AI-04-02 验收 | ✅ 已验证 | ### P1 | ID | 问题 | 证据 | 影响 | |----|------|------|------| | **P1-01** | **WorkerModule 未导入 AiJobModule** | commit `4447569` 撤下了 `b952f5e` 添加的 `AiJobModule` import。当前 `worker.module.ts` 仅注入 `OutboxRepository`,无 `AiJobModule`。`AiInteractiveJobWorker` 的 `@Optional() AI_JOB_EXECUTION_ENGINE` 解析为 `undefined`,Worker 静默跳过所有 Unified Job(`:43-49`) | **Worker 不执行任何 Unified Job。Job 创建后卡在 `queued` 状态永不完成。** 测试环境/白名单验证前必须修复。 | | **P1-02** | **Engine 执行层观测计数器未集成** | `ActiveRecallObservabilityService` 中 `incrementUnifiedExecuteSuccess/Failed/Retry`、`incrementProjectorFailed`、`logExecutionCompleted/Failed` 从未被 Engine 调用(grep 返回空) | 执行层完全盲区:无法观测 Worker 是否正常执行、成功率、平均耗时。白名单验证期间无法判断 Unified 健康状况。 | ### P2 | ID | 问题 | |----|------| | P2-01 | E2E `beforeAll` 中 `fail()` 未从 `@jest/globals` 导入 → `ReferenceError` | | P2-02 | `knowledgeItemContent` 仍为空字符串(M-AI-04-01 遗留 #5) | | P2-03 | FocusItem `knowledgeBaseId` 恒为 `unknown`(M-AI-04-01 遗留 #3) | | P2-04 | ModelRouter preferred/fallback 相同(M-AI-04-01 遗留 #7) | | P2-05 | ReviewCard 无 `jobId` 关联字段(表结构限制) | ### P3 Worker SIGKILL、多 Dispatcher 压力、性能优化、旧代码清理。**不阻塞。** --- ## 11. 无法确认项 1. **CI Docker MySQL/Redis 就绪状态**:本地无 infra,E2E 未真实执行。需运维确认 CI 环境。 2. **P1-01 撤下 AiJobModule 的根因**:commit `4447569` 标注 "pre-existing M-AI-01-09 issue"。需确认是否存在模块循环依赖或其他阻塞性技术问题。 3. **FeatureFlag Redis 30s 缓存在生产的表现**:灰度切换时 30s 延迟可接受,需生产确认。 --- ## 12. 是否允许进入 M-AI-05 **仅白名单后。** 修复 P1-01(WorkerModule 导入 AiJobModule)并确认 E2E CI 通过后,可升级为 PASS。 升级路径: 1. 修复 P1-01:`WorkerModule` 导入 `AiJobModule` 2. 修复 P2-01:E2E `fail()` → `throw new Error()` 3. CI Docker MySQL/Redis 就绪 → E2E 全部通过 4. → **PASS**,允许进入 M-AI-05 --- ## 13. 最终回复 ``` M-AI-04-GATE:CONDITIONAL PASS 是否允许进入 M-AI-05:仅白名单后(修复 P1-01 后可升级为 PASS) 是否允许停止 Legacy 接收新请求:否 ```