P2-06: AiInteractiveJobWorker @Optional() + silent return — must remove
before production Unified to prevent silent job skipping.
P2-07: Engine if (jobType === active_recall) ×4 — should refactor
to Definition lifecycle hooks or Observer pattern. Does not block M-AI-05.
Also update P1-01/P1-02 to reflect BATCH-02 fixes.
Co-Authored-By: Claude <noreply@anthropic.com>
10 KiB
M-AI-04 GATE 独立审核报告
审核日期:2026-06-21 角色:独立审核代理(非 M-AI-04 开发执行者) 基线:M-AI-03 GATE PASS
5108a9aHEAD: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>
- 稳定业务标识:
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
无。 P1-01 和 P1-02 已在 BATCH-02(f8bd950)修复。
P2
| ID | 问题 | 来源 | 计划 |
|---|---|---|---|
| P2-04 | @Optional() 静默吞任务:AiInteractiveJobWorker 使用 @Optional() @Inject(AI_JOB_EXECUTION_ENGINE) + if (!this.engine) return。若模块依赖再次出错,Worker 静默跳过所有 Unified Job 而非启动失败。 |
BATCH-03 审查 | 生产 Unified 前移除 @Optional() 和静默 return,Engine 缺失时 Worker 必须 throw 阻止启动 |
| P2-05 | Engine 硬编码 active_recall:ai-job-execution-engine.ts 中 4 处 if (job.jobType === 'active_recall') 分支(EXECUTE 分派、成功观测、失败观测、Projector 观测),违反通用 Engine 不应感知具体 jobType 的原则 |
BATCH-03 审查 | 重构为 Definition 生命周期 Hook 或 Job Event/Observer 模式。不阻塞 M-AI-05 |
P2(遗留 + 新增)
| ID | 问题 | 阻塞 M-AI-05? |
|---|---|---|
| P2-01 | E2E fail() → throw new Error() 已修复 |
否 |
| 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 关联字段(表结构限制) |
否 |
| P2-06 | @Optional() 静默吞任务 — AiInteractiveJobWorker 中 if (!this.engine) return 会在模块依赖再次出错时静默跳过 Unified Job,而非启动失败 |
生产 Unified 前移除 |
| P2-07 | Engine 硬编码 active_recall — 通用 Engine 中 4 处 if (job.jobType === 'active_recall') 分支,应重构为 Definition Hook 或 Observer 模式 |
否(不阻塞 M-AI-05) |
P3
Worker SIGKILL、多 Dispatcher 压力、性能优化、旧代码清理。不阻塞。
11. 无法确认项
- CI Docker MySQL/Redis 就绪状态:本地无 infra,E2E 未真实执行。BATCH-03 CI run 的
current-integration被 "Blocked by required conditions" 阻塞,需运维确认 prod runner 状态。 - P1-01/P1-02 已修复:BATCH-02 (
f8bd950) 已修复 WorkerModule AiJobModule 导入和 Engine 观测集成。 - FeatureFlag Redis 30s 缓存在生产的表现:灰度切换时 30s 延迟可接受,需生产确认。
12. 是否允许进入 M-AI-05
仅白名单后。 修复 P1-01(WorkerModule 导入 AiJobModule)并确认 E2E CI 通过后,可升级为 PASS。
升级路径:
- 修复 P1-01:
WorkerModule导入AiJobModule - 修复 P2-01:E2E
fail()→throw new Error() - CI Docker MySQL/Redis 就绪 → E2E 全部通过
- → PASS,允许进入 M-AI-05
13. 最终回复
M-AI-04-GATE:CONDITIONAL PASS
是否允许进入 M-AI-05:仅白名单后(修复 P1-01 后可升级为 PASS)
是否允许停止 Legacy 接收新请求:否