api-server/docs/architecture/m-ai-03-current-execution-audit.md
wangdl ed783967e8
All checks were successful
Deploy API Server / build-and-unit (push) Successful in 33s
Deploy API Server / current-integration (push) Successful in 29s
Deploy API Server / backward-compat (push) Successful in 0s
Deploy API Server / deploy (push) Successful in 58s
docs: M-AI-03 审计现有 Job 创建、Worker 执行与 Outbox 接入边界
产出 docs/architecture/m-ai-03-current-execution-audit.md。

修正:
- 移除死代码 publishAsync() 的 Producer 身份(零调用方)
- 移除 NotificationsService.send() 的 Producer 身份(仅写 DB,不入队)
- 移除 AdminEventsController 的 Producer 身份(只读+重试,不创建 Job)
- 新增孤儿队列节:notification/domain-events/audit-logs 的 Consumer 存在但无活跃 Producer

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-20 16:56:39 +08:00

475 lines
26 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# M-AI-03 现有执行边界审计
> 审计日期2026-06-20
> 审计范围:`api-server` 仓库所有 Job 创建、Worker 执行、Queue Producer、Provider 调用、EventBus 与 Outbox 路径
> 审计方法:全文搜索 + 逐文件阅读 + 调用链追踪
---
## 1. 总体架构概览
```
┌────────────────────────────────────────────────────────────┐
│ api-server │
│ │
│ Job System A: Legacy AiJob (BullMQ) │
│ ┌──────────────────────────────────────────────────┐ │
│ │ AiAnalysisService → AiJob DB → BullMQ │ │
│ │ → ai-analysis queue → AiAnalysisWorker │ │
│ │ → AiGatewayService → DeepSeek → AiAnalysisResult│ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ Job System B: AiRuntimeJob (REST Poll) │
│ ┌──────────────────────────────────────────────────┐ │
│ │ UserAiService → AiRuntimeJob DB → Runtime polls │ │
│ │ via REST → RuntimeInternalService │ │
│ │ → zhixi-heavy-runtime → submitResult │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ Job System C: DocumentImport (BullMQ) │
│ ┌──────────────────────────────────────────────────┐ │
│ │ DocumentImportService → DocumentImport DB │ │
│ │ → BullMQ document-import queue │ │
│ │ → DocumentImportWorker → KnowledgeImportWorkflow│ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ Supporting Queues (BullMQ) │
│ ┌──────────────────────────────────────────────────┐ │
│ │ notification, domain-events, audit-logs, │ │
│ │ file-cleanup │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ Outbox (exists, unused) │
│ ┌──────────────────────────────────────────────────┐ │
│ │ OutboxRepository — 表就绪,无生产者,无 Dispatcher │ │
│ └──────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────┘
```
**关键发现**:存在 **两套独立的 Job 系统**AiJob + AiRuntimeJob使用完全不同的调度机制BullMQ vs REST Poll。DocumentImport 使用独立的 model 和队列。Outbox Repository 已实现但没有任何调用方。
---
## 2. Job Producer 矩阵(仅含活跃生产者)
| # | Producer Class | Method | Trigger | DB Table | Queue | Payload | 状态写入 | 代码位置 |
|---|---------------|--------|---------|----------|-------|---------|----------|----------|
| P1 | `AiAnalysisService` | `analyze()` | User API | `AiJob` | `ai-analysis` | `{jobId, userId, type:'active-recall', questionText, knowledgeItemContent, userAnswer}` | `status:pending`, `lifecycleStatus:queued` | `ai-analysis.service.ts:19-31` |
| P2 | `AiAnalysisService` | `evaluateFeynman()` | User API | `AiJob` | `ai-analysis` | `{jobId, userId, type:'feynman-evaluation', knowledgeItemTitle, knowledgeItemContent, userExplanation}` | `status:pending`, `lifecycleStatus:queued` | `ai-analysis.service.ts:40-51` |
| P3 | `DocumentImportService` | `createImport()` | User API | `DocumentImport` | `document-import` | `{importId, userId, knowledgeBaseId, rawText, fileName}` | `status:QUEUED`(via Repository) | `document-import.service.ts:43-49` |
| P4 | `KnowledgeSourceService` | `addSource()` | User API | `DocumentImport` | `document-import` | `{importId, userId, knowledgeBaseId, sourceId, fileName}` | `status:QUEUED`(via Repository) | `knowledge-source.service.ts:43-50` |
| P5 | `KnowledgeSourceService` | `triggerParse()` | User API | `DocumentImport` | `document-import` | `{importId, userId, knowledgeBaseId, sourceId, fileName}` | `status:QUEUED`(via Repository) | `knowledge-source.service.ts:90-97` |
| P6 | `UserAiService` | `requestJob()` | User API | `AiRuntimeJob` | **无 BullMQ** | REST Poll不适用 | `status:pending` | `user-ai.service.ts:282-299` |
| P7 | `AdminFilesController` | COS cleanup | Admin API | 无 | `file-cleanup` | `{objectKey, bucket, region}` | 无(仅入队) | `admin-files.controller.ts:41` |
### 死代码(定义为入队方法但零调用方)
| 方法 | 文件 | 入队目标 | 说明 |
|------|------|---------|------|
| `EventBusService.publishAsync()` | `event-bus.service.ts:26-35` | `domain-events` | **零调用方** — 全文搜索确认无任何代码调用此方法;`domain-events` 队列当前无活跃生产者 |
### 孤儿队列Consumer 已注册但生产者未找到)
| 队列 | Consumer | 状态 |
|------|----------|------|
| `notification` | `NotificationWorker` (`workers/notification.worker.ts:8`) | ⚠️ 无 `queue.add('notification', ...)` 调用方;`NotificationsService.send()` 仅写 DB + sync eventBus.publish(),不入队 |
| `domain-events` | 无专用 Consumer`EventBusService.publishAsync` 是唯一入队路径但为零调用方) | ⚠️ 完全闲置 |
| `audit-logs` | `AuditLogProcessor` (`modules/admin-audit-log/audit-log.processor.ts:7`) | ⚠️ 无 `queue.add('audit-logs', ...)` 调用方;审计日志走 `prisma.adminAuditLog.create()` 直接写 DB |
> **注意**`AdminEventsController` 仅**读取**队列状态(`getWaitingCount`/`getActiveCount`/`getFailed` 等)和**重试**已有失败 Job`job.retry()`**从不创建新 Job**。因此不列为 Producer。
### Producer 代码证据
**P1P2** `src/modules/ai-analysis/ai-analysis.service.ts:19-31, 40-51`
```typescript
const job = await this.repository.createJob(userId, 'active-recall', input.sessionId, input.answerId);
await this.queue.add('ai-analysis', { jobId: job.id, userId, type: 'active-recall', ... });
```
**P3** `src/modules/document-import/document-import.service.ts:43-49`
```typescript
const job = await this.repository.create(dto);
await this.queue.add('document-import', { importId: job.id, userId, knowledgeBaseId, rawText, fileName });
```
**P4P5** `src/modules/knowledge-source/knowledge-source.service.ts:44-50, 91-97`
```typescript
const importJob = await this.importRepo.create({ ... });
await this.queue.add('document-import', { importId: importJob.id, userId, ... });
```
**P6** `src/modules/ai-runtime/user-ai.service.ts:282-299`
```typescript
const job = await this.prisma.aiRuntimeJob.create({ data: { userId, jobType, status: 'pending', ... } });
```
**P7** `src/modules/files/admin-files.controller.ts:41`
```typescript
await this.queue.add(QUEUE_FILE_CLEANUP, { objectKey: file.objectKey, bucket: file.bucket, region: 'ap-beijing' });
```
### 死代码与孤儿队列证据
**`publishAsync()` — 零调用方** `src/common/event-bus/event-bus.service.ts:26-35`
```typescript
async publishAsync(event: BaseDomainEvent): Promise<string> {
if (!this.queue) return '';
const job = await this.queue.add('domain-events', { eventType, eventId, payload, occurredAt });
return job.id || '';
}
// 全文搜索确认:整个代码库中无任何代码调用 publishAsync()
```
**`NotificationsService.send()` — 仅写 DB不入队** `src/modules/notifications/notifications.service.ts:44-49`
```typescript
async send(data: { userId: string; type: string; title: string; body: string }) {
const notification = await this.repository.create(data); // 仅写 DB
this.eventBus?.publish(new NotificationSentEvent(...)); // sync 事件,不入队
return notification;
}
```
**`AdminEventsController` — 只读 + 重试,不创建新 Job** `src/modules/admin-events/admin-events.controller.ts:1-163`
- 所有方法:`getWaitingCount`, `getActiveCount`, `getFailedCount`, `getJob()`, `job.retry()` — 无 `queue.add()`
---
## 3. Worker / Processor 矩阵
| # | Worker Class | Queue | 所在 Module | 注入的 Provider | 结果写入 | 发布事件 |
|---|-------------|-------|------------|-----------------|---------|---------|
| W1 | `AiAnalysisWorker` | `ai-analysis` | `WorkerModule` | `ActiveRecallAnalysisWorkflow`, `FeynmanEvaluationWorkflow`, `AiAnalysisRepository`, `EventBusService?`, `FocusItemsService?` | `AiAnalysisResult` | `ai.analysis.completed` |
| W2 | `DocumentImportWorker` | `document-import` | `WorkerModule` | `DocumentImportRepository`, `KnowledgeItemsRepository`, `KnowledgeImportWorkflow`, `RedisService` | `KnowledgeItem` (多个) | 无 |
| W3 | `NotificationWorker` | `notification` | `WorkerModule` | `NotificationsService` | `Notification` 记录 | 无 |
| W4 | `AuditLogProcessor` | `audit-logs` | `WorkerModule` | `PrismaService` | `AdminAuditLog` | 无 |
| W5 | `FileCleanupProcessor` | `file-cleanup` | `WorkerModule` | `CosStorageProvider` | COS 删除 | 无 |
### Worker 代码证据
**W1** `src/workers/ai-analysis.worker.ts:18-106`
- 行 48: `await this.repository.updateJobStatus(jobId, 'processing')`
- 行 5964: 调用 `feynmanWorkflow.execute()` / `recallWorkflow.execute()`
- 行 6768: `createResult()` + `updateJobStatus(jobId, 'completed')`
- 行 72: `this.eventBus?.publish(new AIAnalysisCompleted({...}))`
- 行 8695: 对每个 weakness 创建 `FocusItem`
**W2** `src/workers/document-import.worker.ts:11-92`
- 行 4245: rawText 为空时直接标记 completed
- 行 5053: 否则 → Redis 写进度,调用 `workflow.execute()`
- 行 6577: 逐个创建 `KnowledgeItem`
- 行 7982: `updateStatus(importId, 'completed')`
---
## 4. AiRuntimeJob 完整链路System B — REST Poll
```
User API → UserAiService.requestJob()
→ SnapshotBuilder.buildSnapshot()
→ prisma.aiRuntimeJob.create({ status: 'pending' })
→ prisma.questionGenerationPlan / flashcardGenerationPlan (if applicable)
→ return { jobId, status: 'pending' }
zhixi-heavy-runtime (external) → RuntimeInternalService.pollJobs()
→ prisma.aiRuntimeJob.findMany({ status: 'pending', jobType: { in: [...] } })
→ filter by snapshotVersion / outputSchemaVersion capacity
→ return { jobs: [...] }
zhixi-heavy-runtime → RuntimeInternalService.lockJob()
→ CAS updateMany(status:pending → status:locked, lockUntil:now+60s)
zhixi-heavy-runtime → RuntimeInternalService.heartbeatJob()
→ updateMany(status:locked → status:running, startedAt:now)
→ extend lockUntil (status:running)
→ check cancelRequestedAt
zhixi-heavy-runtime → RuntimeInternalService.getSnapshot()
zhixi-heavy-runtime → RuntimeInternalService.resolveCredential()
zhixi-heavy-runtime → RuntimeInternalService.submitResult()
→ prisma.aiRuntimeResult.create()
→ prisma.aiRuntimeJob.update(status:succeeded)
→ persistResult() → AiLearningAnalysis / WeakPointCandidate / NextActionRecommendation / Quiz / Flashcard
→ notifyJobComplete() → Notification
zhixi-heavy-runtime → RuntimeInternalService.submitFailure()
→ retry: status:pending, retryCount++
→ exhausted: status:failed, notifyJobComplete()
```
### 关键代码位置
| 步骤 | 文件 | 行号 |
|------|------|------|
| 创建 Job | `src/modules/ai-runtime/user-ai.service.ts` | 270298 |
| Poll | `src/modules/ai-runtime/internal/runtime-internal.service.ts` | 2281 |
| Lock (CAS) | 同上 | 85114 |
| Heartbeat | 同上 | 118149 |
| Get Snapshot | 同上 | 153198 |
| Resolve Credential | 同上 | 201217 |
| Submit Result | 同上 | 220284 |
| Persist (by jobType) | 同上 | 286400 |
| Submit Failure | 同上 | 572625 |
| Notify Complete | 同上 | 629646 |
| Invocation Logs | 同上 | 650694 |
| Cancel (user API) | `src/modules/ai-runtime/user-ai.service.ts` | 343364 |
| Reaper (stuck jobs) | `src/modules/ai-runtime/job-reaper.service.ts` | 24115 |
---
## 5. AiGatewayService — AI Provider 统一网关
### 调用关系
```
AiGatewayService
├── RAG Chat (rag-chat.service.ts:174,249)
├── Vector Service (vector.service.ts:147)
├── Active Recall Workflow (active-recall-analysis.workflow.ts:15)
├── Feynman Workflow (feynman-evaluation.workflow.ts:15)
├── Knowledge Import Workflow (knowledge-import.workflow.ts:14)
├── Learning Trend Workflow (learning-trend.workflow.ts:28)
└── Review Card Workflow (review-card-generation.workflow.ts:15)
```
### 内部结构
`src/modules/ai/gateway/ai-gateway.service.ts:25-271`
- **Retry**: `ModelRouter.resolve(tier)``preferred` provider → fallback on error → `fallback` provider
- **Safety**: `ContentSafetyService.check()` before returning output
- **Cost**: `AiCostCalculatorService.calculate()` per call
- **Usage Log**: `AiUsageLogService.log()` every attempt
- **Event**: `AIUsageRecorded` event on success, `ModelFallbackTriggered` on fallback
- **Parse**: 3-layer JSON extraction (direct → markdown fence → regex)
- **Stream**: `generateStream()` method for SSE use cases
---
## 6. 队列配置矩阵
`src/infrastructure/queue/queue-definitions.ts:94-101` + `src/infrastructure/queue/queue.constants.ts:1-7`
| Queue Name | concurrency | lockDuration | stalledInterval | maxStalledCount | attempts | backoff | 可环境变量覆盖 |
|------------|-------------|-------------|-----------------|-----------------|----------|---------|----------------|
| `ai-analysis` | 1 | 30s | 30s | 1 | 3 | exponential, 1s | `BULL_AI_ANALYSIS_*` |
| `document-import` | 1 | 30s | 30s | 1 | 3 | exponential, 1s | `BULL_DOCUMENT_IMPORT_*` |
| `notification` | 1 | 30s | 30s | 1 | 3 | exponential, 1s | `BULL_NOTIFICATION_*` |
| `domain-events` | 1 | 30s | 30s | 1 | 3 | exponential, 1s | `BULL_DOMAIN_EVENTS_*` |
| `audit-logs` | 1 | 30s | 30s | 1 | 3 | exponential, 1s | `BULL_AUDIT_LOGS_*` |
| `file-cleanup` | 1 | 30s | 30s | 1 | 3 | exponential, 1s | `BULL_FILE_CLEANUP_*` |
**所有队列 concurrency 均为 1**,没有业务超时(只有 BullMQ 锁机制)。
---
## 7. EventBus 使用矩阵
`src/common/event-bus/event-bus.service.ts:10-37`
| 调用方 | Event Type | 发布方式 | 触发时机 |
|--------|-----------|---------|---------|
| `AiAnalysisWorker` | `ai.analysis.completed` | sync | Job 完成后 |
| `AiGatewayService` | `ai.usage.recorded` | sync | AI 调用成功后 |
| `AiGatewayService` | `ai.fallback.triggered` | sync | Provider 降级时 |
| `GrowthService` | `StreakUpdatedEvent` | sync | 学习连续天数更新 |
| `WorkspaceService` | `ItemFavoritedEvent` | sync | 收藏操作 |
| `WorkspaceService` | `ItemUnfavoritedEvent` | sync | 取消收藏 |
| `WorkspaceService` | `TagCreatedEvent` | sync | 创建标签 |
| `WorkspaceService` | `TagDeletedEvent` | sync | 删除标签 |
| `WorkspaceService` | `SearchPerformedEvent` | sync | 执行搜索 |
| `NotificationsService` | `NotificationReadEvent` | sync | 通知已读 |
| `NotificationsService` | `NotificationSentEvent` | sync | 发送通知 |
| `NotificationsService` | `NotificationPreferenceChangedEvent` | sync | 偏好变更 |
| `QueueService` | `task.enqueued` | sync | 入队后 |
### 关键发现
- **全部 sync 发布** — `publishAsync()` 方法已定义但**全文搜索确认为死代码**(零调用方);`domain-events` 队列当前无活跃生产者
- EventBus 当前仅作为 In-Process Event Emitter 使用其异步域事件能力BullMQ `domain-events` 队列)处于闲置状态
- `QueueService` 中同步发布的 `task.enqueued` 事件无 Subscriber 消费
---
## 8. Outbox 现状评估
### Repository 分析
`src/infrastructure/outbox/outbox.repository.ts:27-139`
| 能力 | 实现状态 | 备注 |
|------|---------|------|
| `createInTransaction(tx, input)` | ✅ 已实现 | 接受外部 `Prisma.TransactionClient` |
| `findDispatchable(limit)` | ✅ 已实现 | 简单 `findMany`,无锁 |
| `markProcessing(eventId, lockedBy)` | ✅ 已实现 | CAS via `updateMany(status:pending → processing)` |
| `markPublished(eventId)` | ✅ 已实现 | 简单 update |
| `markFailed(eventId, ...)` | ✅ 已实现 | increment attemptCount |
| `releaseExpiredLocks(thresholdMs)` | ✅ 已实现 | 重置超时 processing → pending |
| **是否有生产写入** | ❌ 无 | **所有搜索返回零调用方** |
| **是否有 Dispatcher** | ❌ 无 | 没有 Dispatcher Service/Worker |
| **是否支持并发领取** | ⚠️ 部分 | CAS 保证了唯一领取,但 `findDispatchable` 可能多 Worker 读到相同事件CAS 后有竞态窗口) |
| **是否使用 SKIP LOCKED** | ❌ 否 | 使用应用层 CASupdateMany + status check非数据库层 SKIP LOCKED |
| **重复发布风险** | ⚠️ 存在 | `findDispatchable` 读取后 `markProcessing` 若 CAS 失败不重试;若 Dispatcher 崩溃后重启,`releaseExpiredLocks` 可恢复但非实时 |
### 表结构 (Prisma)
`prisma/schema.prisma:2323` — 拥有 `id`, `eventType`, `aggregateType`, `aggregateId`, `dedupeKey`, `payload`, `status`, `attemptCount`, `availableAt`, `lockedAt`, `lockedBy`, `publishedAt`, `lastErrorCode`, `lastErrorMessage`
**dedupeKey 有 UNIQUE 约束** → 幂等保证存在
---
## 9. Job 状态机现状
### AiJobLegacy
`src/modules/ai-analysis/ai-analysis.repository.ts:8-16`
```
pending → processing → completed / failed
```
- `lifecycleStatus` (M-AI-02-10 Shadow Write): `pending→queued`, `processing→running`, `completed→succeeded`, `failed→failed`
- 状态由 `AiAnalysisWorker` 直接写入
- **无锁机制** — 依赖 BullMQ 内置的 stalled job 检测
- **无取消支持**
- **无 retry/reaper** — 完全依赖 BullMQ 的 attempts/backoff
### AiRuntimeJob
`src/modules/ai-runtime/job-reaper.service.ts` + `runtime-internal.service.ts`
```
pending → locked → running → succeeded / failed / cancelled
↑ ↓ ↓
└── retried ←── expired ←── (timeout)
```
- 通过 `lockedBy` + `lockUntil` 实现分布式锁
- `RuntimeInternalService.lockJob()` CAS 抢锁
- `RuntimeInternalService.heartbeatJob()` 续约
- `JobReaperService.reap()` 每 30s 收割过期锁和超时 running
- `cancelRequestedAt``cancelledAt` → 取消路径
- 重试:`retryCount < maxRetryCount` → 重置为 `pending`;否则 → `failed`
---
## 10. 超时 / 重试 / 取消矩阵
| 系统 | 超时机制 | 重试次数 | 重试间隔 | 取消支持 | 文件位置 |
|------|---------|---------|---------|---------|---------|
| AiJob (BullMQ) | BullMQ lockDuration=30s, stalledInterval=30s, maxStalledCount=1 | 3 | exponential 1s | ❌ | `queue-definitions.ts:52-57` |
| AiRuntimeJob | timeoutSeconds=120s, Reaper 30s | maxRetryCount=3 | N/A (reaper) | ✅ cancelRequestedAt → cancelledAt | `job-reaper.service.ts`, `user-ai.service.ts:343-364` |
| DocumentImport (BullMQ) | BullMQ lockDuration=30s | 3 | exponential 1s | ❌ | `queue-definitions.ts:96` |
| AiGatewayService | DEFAULT_TIMEOUT_MS=30000, AbortController | tierConfig.maxRetries | sequential attempts | ❌ | `ai-gateway.service.ts:27,51-151` |
---
## 11. 依赖边界分析
### 必须在 WorkerModule 的模块
| 模块 | 原因 | 证据 |
|------|------|------|
| `AiAnalysisWorker` | 仅 Worker 侧 BullMQ Processor不应在 App 中注册 | `worker.module.ts:74` |
| `DocumentImportWorker` | 同上 | `worker.module.ts:75` |
| `NotificationWorker` | 同上 | `worker.module.ts:76` |
| `AuditLogProcessor` | 仅后台审计日志写入 | `worker.module.ts:77` |
| `FileCleanupProcessor` | 仅后台文件清理 | `worker.module.ts:78` |
### 必须在 AppModule + WorkerModule 共用的模块
| 模块 | 原因 | 证据 |
|------|------|------|
| `AiAnalysisModule` | API 侧创建 Job → `AiAnalysisService`Worker 侧消费 → `AiAnalysisWorker` | `app.module.ts:40`, `worker.module.ts:20` |
| `DocumentImportModule` | API 侧创建 Import → `DocumentImportService`Worker 侧消费 → `DocumentImportWorker` | `app.module.ts:37`, `worker.module.ts:21` |
| `AiModule` | API 侧 RAG/Vector 调用 AiGatewayWorker 侧 Workflows 调用 AiGateway | `app.module.ts:10`, `worker.module.ts:8` |
| `EventBusModule` | 双向API 侧发布事件Worker 侧发布/消费事件 | `app.module.ts:9`, `worker.module.ts:24` |
| `NotificationsModule` | API 侧 CRUDWorker 侧 NotificationWorker | `app.module.ts:44`, `worker.module.ts:23` |
### 仅在 AppModule 的模块(不涉及 Worker
| 模块 | 原因 |
|------|------|
| `AiRuntimeModule` | REST Poll 模式Runtime 外部消费,无 BullMQ Worker |
### 循环依赖检查
| 路径 | 状态 | 说明 |
|------|------|------|
| `EventBusService``QueueService` | ⚠️ `forwardRef` 解决 | `event-bus.service.ts:15``@Inject(forwardRef(() => require(...QueueService)))` |
| `AiAnalysisModule``AiModule` (via Workflow) → AiGateway → `EventBusService``QueueService` | ✅ 单向 | 无需 forwardRef |
| API ↔ Worker 循环 | ✅ 无 | Worker 仅消费队列,不调用 API 端点 |
---
## 12. 现状总结与 M-AI-03 风险点
### 必须保持的旧代码
1. **`AiAnalysisRepository`** — 当前 AI Job 创建的唯一入口P1、P2 路径)
2. **`AiAnalysisWorker`** — 处理 active-recall 和 feynman-evaluation 的已有业务
3. **`DocumentImportWorker`** — 已有文档导入链路
4. **`RuntimeInternalService`** — AiRuntimeJob 的 poll/lock/submit 链路zhixi-heavy-runtime 依赖)
5. **`JobReaperService`** — AiRuntimeJob 的过期锁收割
6. **All 6 BullMQ queues** — 已有业务依赖
### 可以独立新增的模块
1. **新 Job Engine** — 在 M-AI-02 Schema Expand 基础上纯代码层实现
2. **新 Outbox Dispatcher** — 独立 Service/Worker不修改已有队列
3. **新 Registry** — 独立 Module不依赖已有 Processor
4. **新状态机** — 独立于现有 `AiAnalysisRepository.STATUS_TO_LIFECYCLE`
5. **新 Projector** — 独立于现有 `RuntimeInternalService.persistResult()`
### M-AI-03 需要避免的冲突
- **不修改** `AiAnalysisWorker` 的业务逻辑
- **不修改** `RuntimeInternalService` 的 REST 接口heavy-runtime 依赖)
- **不修改** `AiGatewayService` 的 provider 调用链
- **不修改** BullMQ 队列定义(已有队列保持)
- **新 Engine 不接管** 已有 `ai-analysis``document-import` 队列
- **新代码仅在** `AiJob` 表(即 `AiAnalysisJob`),不碰 `AiRuntimeJob`
### 关键风险
| 风险 | 严重度 | 说明 |
|------|--------|------|
| 两套 Job 状态不同步 | 🔴 高 | `AiJob.status`(legacy enum) 与 `AiJob.lifecycleStatus`(M-AI-02 新字段) 存在映射但不完整 |
| Outbox 无 Dispatcher | 🟡 中 | 需新建但独立 |
| `forwardRef` EventBus↔Queue | 🟢 低 | 已有解决方案,不扩大 |
| DocumentImport 使用独立 model | 🟡 中 | 不在 M-AI-03 范围,但未来统一需注意 |
| RuntimeInternal 无事务保证 | 🔴 高 | result + job update 分两步,非原子 |
| 孤儿队列notification/domain-events/audit-logs | 🟡 中 | Consumer 已注册但无活跃 Producer若从未有消息入队`NotificationWorker``AuditLogProcessor` 实为闲置进程
---
## 附录 A完整文件清单
| 文件 | 角色 |
|------|------|
| `src/modules/ai-analysis/ai-analysis.repository.ts` | AiJob 持久层 |
| `src/modules/ai-analysis/ai-analysis.service.ts` | AiJob Producer |
| `src/modules/ai-analysis/ai-analysis.module.ts` | 模块注册 |
| `src/workers/ai-analysis.worker.ts` | AiJob Consumer |
| `src/modules/document-import/document-import.service.ts` | Document Import Producer |
| `src/modules/document-import/document-import.repository.ts` | Document Import 持久层 |
| `src/workers/document-import.worker.ts` | Document Import Consumer |
| `src/modules/knowledge-source/knowledge-source.service.ts` | Knowledge Source Producer间接 |
| `src/workers/notification.worker.ts` | Notification Consumer |
| `src/modules/admin-audit-log/audit-log.processor.ts` | Audit Log Consumer |
| `src/modules/files/file-cleanup.processor.ts` | File Cleanup Consumer |
| `src/infrastructure/queue/queue.service.ts` | QueueService — 统一入队入口 |
| `src/infrastructure/queue/queue-definitions.ts` | 队列定义注册表 |
| `src/infrastructure/queue/queue.constants.ts` | 队列名常量 |
| `src/infrastructure/queue/queue.module.ts` | 队列模块BullMQ 注册) |
| `src/infrastructure/outbox/outbox.repository.ts` | OutboxRepository无调用方 |
| `src/common/event-bus/event-bus.service.ts` | EventBus — 同步/异步发布 |
| `src/modules/ai/gateway/ai-gateway.service.ts` | AI 网关 — 统一 Provider 路由 |
| `src/modules/ai-runtime/internal/runtime-internal.service.ts` | Runtime Internal REST API |
| `src/modules/ai-runtime/user-ai.service.ts` | AiRuntimeJob Producer + Cancel |
| `src/modules/ai-runtime/job-reaper.service.ts` | AiRuntimeJob Reaper过期锁收割 |
| `src/app.module.ts` | API 进程模块组装 |
| `src/worker.module.ts` | Worker 进程模块组装 |
| `prisma/schema.prisma` | Prisma SchemaAiJob, AiJobSnapshot, AiJobArtifact, AiRuntimeJob, OutboxEvent 等) |