# M-AI-04 Active Recall 迁移契约

> 状态：✅ 已审计冻结（M-AI-04-01 完成）
> 审计日期：2026-06-21
> 基线：M-AI-03 GATE PASS（commit `5108a9a`）
> 对应 Issue：[#296](https://git.admin.longde.cloud/wangdl/api-server/issues/296)
> ⚠️ 行号引用以 commit `5108a9a` 为准。后续代码变更可能导致行号漂移，Review 时请对照该 commit 验证。

---

## 1. 当前链路（已审计确认）

```
POST /api/active-recalls/:id/submit
→ Controller: ActiveRecallController.submit()
    active-recall.controller.ts:21
    @Body() body: any  // ⚠️ 无 DTO 校验，期望 { answerText: string }

→ ActiveRecallService.submit(userId, questionId, body)
    active-recall.service.ts:19-39
    ├── ⚠️ userId 来自 @CurrentUser() → user?.id || 'anonymous'
    │       JwtAuthGuard 作为全局 APP_GUARD (app.module.ts:184) 生效，
    │       但代码中的 || 'anonymous' fallback 暗示防御性编程，
    │       实际上 JWT 校验失败时 Guard 直接返回 401，不会到达 Controller。
    │
    ├── ActiveRecallRepository.findById(questionId)
    │       active-recall.repository.ts:19-21
    │       → prisma.activeRecallQuestion.findUnique({ where: { id } })
    │       → 不存在时 throw NotFoundException('问题不存在')
    │       → ⚠️ P0 安全缺陷：不校验 question.userId === userId
    │         用户 A 可以提交用户 B 的 questionId，导致分析结果错配到 A 名下
    │
    ├── ActiveRecallRepository.createAnswer(userId, questionId, body)
    │       active-recall.repository.ts:41-50
    │       → prisma.activeRecallAnswer.create({ userId, questionId, answerText, submittedAt })
    │       → 表: ActiveRecallAnswer (id, userId, questionId, sessionId, answerType, answerText, audioFileId, submittedAt)
    │       → answerType 默认 "text"
    │
    └── AiAnalysisService.analyze(userId, { questionText, knowledgeItemContent, userAnswer, answerId })
            ai-analysis.service.ts:12-31
            ├── AiAnalysisRepository.createJob(userId, 'active-recall', sessionId, answerId)
            │       ai-analysis.repository.ts:17-33
            │       → prisma.aiJob.create({
            │           userId, jobType: 'active-recall', sessionId, answerId,
            │           status: 'pending',
            │           lifecycleStatus: 'queued',       // M-AI-02-10 Shadow Write
            │           queueName: 'ai-interactive',     // ⚠️ 写入 'ai-interactive' 但实际入队 'ai-analysis'
            │           inputSchemaVersion: 'legacy-v1',
            │           attemptCount: 0,
            │           queuedAt: new Date()
            │         })
            │       → 表: AiJob (物理表名 AiAnalysisJob)
            │
            └── QueueService.add('ai-analysis', { jobId, userId, type: 'active-recall', questionText, knowledgeItemContent, userAnswer })
                    queue.service.ts:47-64
                    → BullMQ queue: 'ai-analysis' (QUEUE_AI_ANALYSIS)
                    → queue.constants.ts:1
                    → 默认 JobOptions (queue-definitions.ts:61-66):
                      { attempts: 3, backoff: { type: 'exponential', delay: 1000 },
                        removeOnComplete: { count: 1000, age: 24*3600 },  // 保留最近 1000 条，24h
                        removeOnFail: { count: 5000, age: 7*24*3600 } }   // 保留最近 5000 条，7d
                    → 写入 TaskLog 表（queueName + jobId + payload + status: 'enqueued'）
                    → 发布 task.enqueued 事件

    ⚠️ analyze() 错误被 catch 并 log，不 re-throw → 答案返回不受 AI 入队结果影响

Worker: AiAnalysisWorker (@Processor('ai-analysis'))
    workers/ai-analysis.worker.ts:18-106
    ├── AiAnalysisRepository.updateJobStatus(jobId, 'processing')
    │       ai-analysis.repository.ts:35-46
    │       → 设置 status='processing', lifecycleStatus='running', startedAt=now
    │
    ├── [type='active-recall'] → ActiveRecallAnalysisWorkflow.execute(input)
    │       modules/ai/workflows/active-recall-analysis.workflow.ts:17-41
    │       └── AiGatewayService.generate({
    │               feature: 'active-recall-analysis',
    │               userId, tier: 'primary',
    │               promptKey: 'active-recall-analysis',
    │               promptVersion: '1.0.0',
    │               messages: [{ role: 'user', content: userMessage }],
    │               outputSchema: ActiveRecallAnalysisResultSchema (Zod)
    │           })
    │           modules/ai/gateway/ai-gateway.service.ts:40-170
    │           ├── ModelRouter.resolve('primary')
    │           │       model-router.ts:70-72
    │           │       → { tier: 'primary', preferred: { provider:'deepseek', model:'deepseek-v4-pro' },
    │           │           fallback: { provider:'deepseek', model:'deepseek-v4-pro' }, maxRetries: 3 }
    │           │       ⚠️ preferred 和 fallback 相同 → 无实际 fallback 效果
    │           │
    │           ├── PromptTemplateService.get('active-recall-analysis', '1.0.0')
    │           │       prompt-template.service.ts:65-73
    │           │       → 硬编码 TypeScript 常量（非 DB）
    │           │       → systemPrompt: ACTIVE_RECALL_ANALYSIS_SYSTEM_PROMPT
    │           │       → outputSchemaDesc: ACTIVE_RECALL_OUTPUT_SCHEMA_DESC
    │           │
    │           ├── DeepSeekProvider.generate({ model, messages, temperature: 0.3, maxTokens: 4096, responseFormat: 'json_object' })
    │           │       → HTTP POST to DeepSeek API
    │           │
    │           ├── ContentSafetyService.check(output.rawText, { contentType: 'ai_output' })
    │           │       → 不安全时 throw Error('AI output blocked by content safety')
    │           │
    │           ├── parseJson() - 3 层 JSON 解析：
    │           │       1. 直接 JSON.parse → Zod schema.parse
    │           │       2. 提取 markdown ```json``` fence
    │           │       3. 提取第一个 {…} 对象
    │           │
    │           ├── AiUsageLogService.log({ userId, feature, provider, model, tier, promptKey, promptVersion, inputTokens, outputTokens, estimatedCost, latencyMs, success })
    │           │       → 表: AiUsageLog (usage-log.service.ts)
    │           │
    │           └── EventBusService.publish(AIUsageRecorded event)
    │
    ├── AiAnalysisRepository.createResult(userId, jobId, result)
    │       ai-analysis.repository.ts:55-69
    │       → prisma.aiAnalysisResult.create({
    │           userId, jobId,
    │           summary: result.summary,
    │           masteryScore: result.score,
    │           strengths: result.strengths (Json),
    │           weaknesses: result.weaknesses (Json),
    │           suggestions: result.focusItems (Json),
    │           nextActions: result.reviewSuggestion (Json),
    │           rawResult: result (Json)
    │         })
    │       → 表: AiAnalysisResult
    │
    ├── AiAnalysisRepository.updateJobStatus(jobId, 'completed')
    │       → 设置 status='completed', lifecycleStatus='succeeded', finishedAt=now
    │
    ├── EventBusService.publish(AIAnalysisCompleted event)
    │       → eventType: 'ai.analysis.completed'
    │       → payload: { userId, jobId, sessionId, answerId, type, score, analysis, timestamp }
    │       └── 消费方: ReviewCardSubscriber.handleAIAnalysisCompleted()
    │               modules/review/review-card.subscriber.ts:11-51
    │               → ReviewService.generateCards(userId, { knowledgeItemTitle, knowledgeItemContent, cardCount })
    │                   modules/review/review.service.ts:68-99
    │                   → ReviewCardGenerationWorkflow.execute() → AiGatewayService.generate()
    │                   → 创建 1-3 条 ReviewCard (SM-2: intervalDays=1, easeFactor=2.5, scheduleState='new')
    │                   → 表: ReviewCard
    │
    └── FocusItemsService.create(userId, { title: w, source: 'ai-analysis', status: 'open' })
            → 为每个 result.weaknesses 元素创建一条 FocusItem
            → knowledgeBaseId: result.knowledgeBaseId || 'unknown'
            → ⚠️ 数据完整性问题：ActiveRecallAnalysisResultSchema 不含 knowledgeBaseId 字段
              (active-recall-analysis.schema.ts:17-28)，因此 result.knowledgeBaseId 恒为 undefined，
              所有 FocusItem 的 knowledgeBaseId 恒为 'unknown'，无法关联到具体知识库
            → 表: FocusItem

错误处理：
    Worker catch → updateJobStatus(jobId, 'failed', err.message) → lifecycleStatus='failed'
    → throw err (触发 BullMQ 重试，默认 3 次指数退避)
```

### 关键发现

| # | 发现 | 严重度 | 文件:行 |
|---|------|--------|---------|
| 1 | `queueName` 写入 `'ai-interactive'`，但实际 BullMQ 入队 `'ai-analysis'` | **P0** | `ai-analysis.repository.ts:28` vs `ai-analysis.service.ts:21` |
| 2 | `ActiveRecallService.submit()` 不校验 `question.userId === userId`，用户 A 可提交用户 B 的问题 | **P0** | `active-recall.service.ts:20-21` |
| 3 | 所有 FocusItem 的 `knowledgeBaseId` 恒为 `'unknown'`，因 AI 输出 Schema 不含此字段 | **P1** | `active-recall-analysis.schema.ts:17-28` → `ai-analysis.worker.ts:89` |
| 4 | POST body 类型为 `any`，无 DTO 校验 | **P1** | `active-recall.controller.ts:21` |
| 5 | `knowledgeItemContent` 硬编码为空字符串 `''`。此外该行注释 `// worker picks up content from the analysis workflow` 是**虚假注释**：Worker 不查询 DB 获取知识点内容，直接使用 Job data 中的 `knowledgeItemContent`（即空字符串）。AI 模型仅收到 `【知识点原文】\n\n` 而无实际内容，分析质量严重受损 | **HIGH** | `active-recall.service.ts:29` → `ai-analysis.service.ts:26` → `active-recall-analysis.workflow.ts:18-21` |
| 6 | `removeOnComplete: { count: 1000, age: 24h }` / `removeOnFail: { count: 5000, age: 7d }` — completed Job 保留 24h（非立即删除），failed Job 保留 7d。影响故障排查窗口和存储容量估算，Unified 链路需匹配此行为 | **INFO** | `queue-definitions.ts:64-65` |
| 7 | ModelRouter `primary` 和 `strong` tier 的 preferred/fallback 完全相同（均为 `deepseek-v4-pro`），`maxRetries: 3`。这意味着 4 次尝试全部打到同一个模型，fallback 机制形同虚设。生产环境中若 deepseek-v4-pro 故障或限流，重试只会重复失败，不会自动切换备用模型 | **HIGH** | `model-router.ts:24-29` |
| 8 | Prompt 硬编码在 TypeScript 常量中，非 DB 管理 | INFO | `active-recall-analysis.prompt.ts` |
| 9 | AI 分析入队失败不阻止答案返回（catch + log） | INFO | `active-recall.service.ts:34-36` |
| 10 | `ActiveRecallAnswer` 包含 `audioFileId` 和 `answerType` 字段（`schema.prisma:554-556`），但当前 `submit()` 仅接受 `{ answerText }`。非文本答案（音频）在 Unified 链路中的处理方式未定义 | **P2** | `active-recall.controller.ts:21` → `schema.prisma:549-566` |
| 11 | Worker stall 恢复（`maxStalledCount: 1`，`queue-definitions.ts:58`）可能导致重复 `AiAnalysisResult`：AI 调用成功后 Worker 崩溃 → BullMQ 重新投递 → 再次执行 → `createResult()` 无幂等保护（`AiAnalysisResult` 无 `@@unique([jobId])`）→ 同一 jobId 产生两条 Result | **HIGH** | `ai-analysis.worker.ts:67` → `ai-analysis.repository.ts:55` → `schema.prisma:679-700` → `queue-definitions.ts:58` |

---

## 2. 目标链路

```
POST /api/active-recalls/:id/submit
→ ActiveRecallService
→ ActiveRecallExecutionRouter (NEW)
  ├─ legacy → 原有 AiAnalysisService 路径（不改动）
  └─ unified → AiJobCreationService
       → AiJob + AiJobSnapshot + OutboxEvent
       → Outbox Dispatcher
       → Queue: ai-interactive
       → AiJobExecutionEngine
       → ActiveRecallExecutor
       → Validation (Business + Reference)
       → ActiveRecallProjector
       → 业务结果 + AiJobArtifact
```

---

## 3. 输入 Snapshot Schema（已冻结）

```json
{
  "schemaVersion": "active-recall-v1",
  "snapshot": {
    "userId": "<string, 必填>",
    "activeRecallId": "<string, 问题 ID，必填>",
    "knowledgeItemId": "<string | null, 问题关联的知识点 ID>",
    "questionText": "<string, 问题原文，必填>",
    "userAnswer": "<string, 用户回答文本，必填>",
    "referenceAnswer": "<string | null, 参考答案/标准内容>",
    "answerId": "<string, ActiveRecallAnswer.id，必填>",
    "submittedAt": "<ISO8601 timestamp, 答案提交时间>",
    "promptKey": "active-recall-analysis",
    "promptVersion": "1.0.0",
    "modelTier": "primary",
    "modelProvider": "deepseek",
    "modelName": "deepseek-v4-pro",
    "maxTokens": 4096,
    "temperature": 0.3
  }
}
```

### 禁止字段

- JWT / Authorization Header
- 模型 API Key
- Cookie
- 数据库连接信息
- 无关用户画像
- 未脱敏 Credential
- 用户邮箱、手机号等 PII

### 区分原则

| 数据 | 归属 | 理由 |
|------|------|------|
| userId, activeRecallId, questionText, userAnswer, answerId | Snapshot（冻结） | 重放 AI 调用所需 |
| knowledgeItemContent | 执行时重新查询 | 知识点内容可能更新 |
| knowledgeItemId | Snapshot（冻结） | 关联追溯 |
| referenceAnswer | 执行时重新查询 | 从 KnowledgeItem 获取 |
| promptKey, promptVersion, modelTier | Snapshot（冻结） | 重放一致性 |
| 用户 email/phone/displayName | 禁止 | PII |

---

## 4. 输出 Schema（已冻结）

```json
{
  "score": "<number, 0-100, 必填>",
  "masteryLevel": "'excellent' | 'good' | 'partial' | 'weak' | 'none', 必填",
  "summary": "<string, 1-2000 字符, 必填>",
  "strengths": ["<string[], 最多 10 项>"],
  "weaknesses": ["<string[], 最多 10 项>"],
  "missingKeyPoints": ["<string[], 最多 20 项>"],
  "misconceptions": ["<string[], 最多 10 项>"],
  "weaknessTypes": ["<string[], missing_detail|missing_application|misconception|vague_expression|incomplete_structure|wrong_emphasis>"],
  "focusItems": [
    {
      "title": "<string, 1-255>",
      "reason": "<string, 1-1000>",
      "suggestion": "<string, optional>",
      "priority": "'high' | 'normal' | 'low'"
    }
  ],
  "reviewSuggestion": {
    "shouldReview": "<boolean>",
    "intervalDays": "<number, 1-365>",
    "cardFront": "<string, optional>",
    "cardBack": "<string, optional>"
  }
}
```

### 验证规则

| 字段 | 规则 | 来源 |
|------|------|------|
| score | 0 ≤ score ≤ 100, 整数 | `active-recall-analysis.schema.ts:18` |
| masteryLevel | enum: excellent/good/partial/weak/none | `active-recall-analysis.schema.ts:19` |
| summary | 1-2000 字符 | `active-recall-analysis.schema.ts:20` |
| strengths | 最多 10 项, 每项 ≤500 字符 | `active-recall-analysis.schema.ts:21` |
| weaknesses | 最多 10 项, 每项 ≤500 字符 | `active-recall-analysis.schema.ts:22` |
| focusItems | 最多 10 项 | `active-recall-analysis.schema.ts:26` |
| reviewSuggestion | 必填, intervalDays 1-365 | `active-recall-analysis.schema.ts:27` |

---

## 5. 副作用矩阵（已审计）

| 操作 | 表/实体 | 触发条件 | 写入方 | 文件:行 |
|------|---------|----------|--------|---------|
| 创建答案记录 | `ActiveRecallAnswer` | 每次提交 | `ActiveRecallRepository.createAnswer()` | `active-recall.repository.ts:41` |
| 创建 Job | `AiJob` (AiAnalysisJob) | 每次提交 | `AiAnalysisRepository.createJob()` | `ai-analysis.repository.ts:17` |
| 创建 TaskLog | `TaskLog` | 每次入队 | `QueueService.add()` | `queue.service.ts:59` |
| 更新 Job → processing | `AiJob` | Worker 开始执行 | `AiAnalysisRepository.updateJobStatus()` | `ai-analysis.worker.ts:48` |
| 调用 AI 模型 | DeepSeek API | Worker 执行 | `AiGatewayService.generate()` | `ai-gateway.service.ts:58` |
| 记录 UsageLog | `AiUsageLog` | AI 调用完成 | `AiUsageLogService.log()` | `ai-gateway.service.ts:78` |
| 发布 AIUsageRecorded | EventBus | AI 调用完成 | `AiGatewayService` | `ai-gateway.service.ts:94` |
| 安全审核 | ContentSafety | AI 输出后 | `ContentSafetyService.check()` | `ai-gateway.service.ts:67` |
| 创建 FallbackEvent | `FallbackEvent` | 首次调用失败切备用 | `AiGatewayService` | `ai-gateway.service.ts:127` |
| 创建分析结果 | `AiAnalysisResult` | Worker 成功后 | `AiAnalysisRepository.createResult()` | `ai-analysis.worker.ts:67` |
| 更新 Job → completed | `AiJob` | Worker 成功后 | `AiAnalysisRepository.updateJobStatus()` | `ai-analysis.worker.ts:68` |
| 发布 AIAnalysisCompleted | EventBus | Worker 成功后 | `AiAnalysisWorker` | `ai-analysis.worker.ts:72` |
| 生成复习卡片 | `ReviewCard` | 收到 AIAnalysisCompleted 事件 | `ReviewCardSubscriber` → `ReviewService.generateCards()` | `review-card.subscriber.ts:39` |
| 创建薄弱项 | `FocusItem` | result.weaknesses.length > 0 | `FocusItemsService.create()` | `ai-analysis.worker.ts:88` | ⚠️ knowledgeBaseId 恒为 'unknown' |
| 更新 Job → failed | `AiJob` | Worker 失败 | `AiAnalysisRepository.updateJobStatus()` | `ai-analysis.worker.ts:102` |

---

## 6. 状态映射（已冻结）

| 业务阶段 | 旧 Job 状态 (`status`) | 新 `lifecycleStatus` | Active Recall 业务状态 | 客户端可见 |
|----------|----------------------|---------------------|----------------------|-----------|
| 提交答案 | `pending` | `queued` | 答案已提交，等待 AI 分析 | 答案已提交 |
| AI 分析执行中 | `processing` | `running` | AI 正在分析回答 | 分析中 |
| 分析完成 | `completed` | `succeeded` | 分析完成，结果可用 | 分析完成（可查看结果） |
| 分析失败 | `failed` | `failed` | 分析失败（自动重试后仍失败） | 分析失败 |
| 取消 | N/A（旧链路不支持） | `cancelled` | 分析已取消 | 分析取消 |

### Shadow Write 映射（ai-analysis.repository.ts:10-15）

```
pending    → queued
processing → running
completed  → succeeded
failed     → failed
```

---

## 7. 幂等键

```
active-recall:<answerId>
```

- **稳定业务标识**：`answerId` — `ActiveRecallAnswer.id`，每次提交生成唯一 ID
- **唯一约束**：`AiJob.@@unique([userId, jobType, idempotencyKey])` (`schema.prisma:636`)
- **冲突处理**：P2002 时返回已有 Job（由 `AiJobCreationService` 实现）
- **格式**：`active-recall:<answerId>`

---

## 8. Feature Flag

| 属性 | 值 |
|------|-----|
| 配置名 | `ACTIVE_RECALL_ENGINE_MODE` |
| 值 | `legacy` \| `unified` |
| 默认 | `legacy`（切换前） |
| 白名单 | 支持用户 ID 白名单（通过 `FeatureFlagService`） |
| 存储 | `FeatureFlag` 表 + Redis 缓存（30s TTL） |
| 分支点 | `ActiveRecallExecutionRouter`（待实现，M-AI-04-05） |
| 切换方式 | 修改 FeatureFlag 值，无需重启 |

### 配置机制（现有基础设施）

- **FeatureFlagService**: `FeatureFlag` 表 (`name`, `enabled`, `whitelist`, `rolloutPct`)，Redis 缓存 30s
  - `config/feature-flag.service.ts`
- **AppConfigService**: `AppConfig` 表 (key-value)，Redis 缓存 60s
  - `config/config.service.ts`

---

## 9. 回滚流程

```
unified → legacy:
1. 修改 ACTIVE_RECALL_ENGINE_MODE=legacy（通过 Admin 或 FeatureFlag API）
2. 新请求立即走 Legacy 路径（ActiveRecallExecutionRouter 读取 Flag）
3. 已创建的新 Job 继续完成或取消（不强制中断）
4. 不重新送入 Legacy（避免重复分析）
5. 客户端仍可查询已有新 Job（GET /api/ai/jobs/:jobId）
6. 已写入的 AiAnalysisResult / FocusItem / ReviewCard 不删除
```

### 禁止事项

- 禁止 Legacy 和 Unified 双链路同时执行同一 answerId（通过幂等键保证）
- 禁止在无运行证据时直接全量切换
- 禁止自动 Legacy fallback（必须通过 Flag 显式切换）

---

## 10. 不确定项

- [x] 确认 `knowledgeItemContent` 来源：当前硬编码 `''`，Worker 不查 DB。迁移后 Snapshot 仍可为空，Executor 可从 knowledgeItemId 查询。
- [x] 确认 `queueName` 不一致：当前 DB 写入 `ai-interactive` 但 BullMQ 路由 `ai-analysis`。迁移后 Unified 路径统一使用 `ai-interactive`。
- [x] 确认 ReviewCard 生成是否需要保留：是，`AIAnalysisCompleted` → `ReviewCardSubscriber` 链路由 EventBus 驱动，与 Job 系统解耦。
- [x] 确认认证/权限缺陷：JwtAuthGuard 为全局 Guard（`app.module.ts:184`），认证层面安全；但 `submit()` 不校验跨用户所有权（P0），需在 M-AI-04-05 修复。
- [x] 确认 FocusItem knowledgeBaseId 恒为 `'unknown'`（P1）：`ActiveRecallAnalysisResultSchema` 不含 `knowledgeBaseId` 字段，需在 M-AI-04-03 输出 Schema 中增加该字段。
- [x] 确认 Job 保留策略：`removeOnComplete: { count: 1000, age: 24h }` / `removeOnFail: { count: 5000, age: 7d }`（`queue-definitions.ts:64-65`），Unified 链路 `ai-interactive` 队列使用相同默认值。
- [ ] 待 M-AI-04-02：Snapshot 是否包含 `knowledgeItemContent`（查询时获取 vs 快照冻结）— 建议：不包含，Executor 执行时实时查询。
- [ ] 待 M-AI-04-03：Executor 是否复用现有 `ActiveRecallAnalysisWorkflow` 还是新建。
- [ ] 待 M-AI-04-05：`ActiveRecallExecutionRouter` 的分支粒度（per-request vs per-user vs per-session）。
- [ ] 待 M-AI-04-03/04：非文本答案（`audioFileId`）在 Unified 链路的处理方式。当前 `submit()` 仅处理 `{ answerText }`，但 `ActiveRecallAnswer` 模型包含 `audioFileId` 和 `answerType` 字段。若支持音频答案，需语音转文本步骤或独立的音频分析 Executor。
- [ ] 待 M-AI-04-04：Worker stall 恢复的重复 `AiAnalysisResult` 风险。`maxStalledCount: 1` + `AiAnalysisResult` 无 `@@unique([jobId])` 约束 → 崩溃重试可能产生重复结果。Unified 链路的 `ActiveRecallProjector` 必须在 Projector 层提供幂等保证。

---

## 关联 Issue

| Issue | 标题 | Gitea |
|-------|------|-------|
| M-AI-04-01 | 审计并冻结迁移契约 | [#296](https://git.admin.longde.cloud/wangdl/api-server/issues/296) ✅ 本 Issue |
| M-AI-04-02 | 注册 Definition 与 Snapshot | [#297](https://git.admin.longde.cloud/wangdl/api-server/issues/297) |
| M-AI-04-03 | Executor 与输出验证 | [#298](https://git.admin.longde.cloud/wangdl/api-server/issues/298) |
| M-AI-04-04 | Projector、Artifact 与幂等写入 | [#299](https://git.admin.longde.cloud/wangdl/api-server/issues/299) |
| M-AI-04-05 | 入口接入 CreationService | [#300](https://git.admin.longde.cloud/wangdl/api-server/issues/300) |
| M-AI-04-06 | 状态兼容、可观测性与回滚 | [#301](https://git.admin.longde.cloud/wangdl/api-server/issues/301) |
| M-AI-04-07 | 真实业务 E2E 与受控切换 | [#302](https://git.admin.longde.cloud/wangdl/api-server/issues/302) |
| M-AI-04-08-GATE | 最终验收与切换 | [#303](https://git.admin.longde.cloud/wangdl/api-server/issues/303) |
