Engine is now a hard dependency — DI resolution failure prevents Worker startup (fail-fast) instead of silently skipping all Unified Jobs. Changes: - ai-interactive.worker.ts: remove @Optional(), silent return guard - ai-background.worker.ts: remove @Optional(), silent return guard - docs: mark P2-06 as fixed Co-Authored-By: Claude <noreply@anthropic.com>
327 lines
11 KiB
Markdown
327 lines
11 KiB
Markdown
# 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>
|
||
```
|
||
|
||
- 稳定业务标识:`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()` 静默吞任务** — ✅ 已修复 (`4f74c09`):`AiInteractiveJobWorker` + `AiBackgroundJobWorker` 移除 `@Optional()` 和 `if (!this.engine) return`。Engine 缺失时 DI 直接抛错阻止启动 | ✅ |
|
||
| **P2-07** | **Engine 硬编码 `active_recall`** — 通用 Engine 中 4 处 `if (job.jobType === 'active_recall')` 分支,应重构为 Definition Hook 或 Observer 模式 | 否(不阻塞 M-AI-05) |
|
||
|
||
### P3
|
||
|
||
Worker SIGKILL、多 Dispatcher 压力、性能优化、旧代码清理。**不阻塞。**
|
||
|
||
---
|
||
|
||
## 11. 无法确认项
|
||
|
||
1. **CI Docker MySQL/Redis 就绪状态**:本地无 infra,E2E 未真实执行。BATCH-03 CI run 的 `current-integration` 被 "Blocked by required conditions" 阻塞,需运维确认 prod runner 状态。
|
||
2. **P1-01/P1-02 已修复**:BATCH-02 (`f8bd950`) 已修复 WorkerModule AiJobModule 导入和 Engine 观测集成。
|
||
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 接收新请求:否
|
||
```
|