import { Injectable, Logger, BadRequestException } from '@nestjs/common'; import * as crypto from 'crypto'; import { FeatureFlagService } from '../config/feature-flag.service'; import { FeynmanSnapshotBuilder } from '../ai-job/feynman-snapshot-builder'; import type { FeynmanSnapshotInput } from '../ai-job/feynman-snapshot-builder'; import { AiJobCreationService } from '../ai-job/ai-job-creation.service'; import { JobDefinitionRegistry } from '../ai-job/job-definition-registry'; import { AiAnalysisService } from './ai-analysis.service'; /** * M-AI-05-05: Feynman Execution Router * * 根据 FEYNMAN_ENGINE_MODE Feature Flag 决定 Feynman 评估的执行分支: * - 'legacy' → 原 AiAnalysisService.evaluateFeynman() 路径 * - 'unified' → FeynmanSnapshotBuilder → AiJobCreationService → Unified Job Engine * * 设计约束(契约 §10): * - 分支判断集中在 Router,不散落在 Controller/Service/Worker * - 支持用户白名单(通过 FeatureFlagService) * - 默认 legacy(Feature Flag 不存在或 disabled 时) * - Unified 失败不得自动调用 Legacy * - 同一请求只能执行一个引擎 */ const FLAG_NAME = 'FEYNMAN_ENGINE_MODE'; /** Feynman HTTP 请求体(与 AiAnalysisController 保持一致) */ export interface FeynmanEvaluateInput { knowledgeItemTitle: string; knowledgeItemContent: string; userExplanation: string; sessionId?: string; answerId?: string; } /** Unified 模式扩展响应 */ export interface FeynmanUnifiedResponse { jobId: string; status: string; engineMode: 'unified'; lifecycleStatus: string; } /** Legacy 兼容响应 */ export interface FeynmanLegacyResponse { jobId: string; status: string; } @Injectable() export class FeynmanExecutionRouter { private readonly logger = new Logger(FeynmanExecutionRouter.name); constructor( private readonly featureFlag: FeatureFlagService, private readonly snapshotBuilder: FeynmanSnapshotBuilder, private readonly creationService: AiJobCreationService, private readonly registry: JobDefinitionRegistry, private readonly legacyService: AiAnalysisService, ) {} /** * 路由 Feynman 评估请求。 * * @param userId - 请求用户 ID * @param input - 请求体(knowledgeItemTitle/content/explanation + 可选的 sessionId/answerId) * @param knowledgeItemId - 知识点 ID(由 Controller 从请求体获取或后续客户端传入) * @returns Legacy 或 Unified 响应 */ async evaluateFeynman( userId: string, input: FeynmanEvaluateInput, knowledgeItemId?: string, ): Promise { // 1. 基本参数校验(与 Legacy 一致) if (!input.knowledgeItemTitle?.trim()) { throw new BadRequestException('knowledgeItemTitle is required'); } if (!input.knowledgeItemContent?.trim()) { throw new BadRequestException('knowledgeItemContent is required'); } if (!input.userExplanation?.trim()) { throw new BadRequestException('userExplanation is required'); } // 2. 检查 Feature Flag const useUnified = await this.shouldUseUnified(userId); if (!useUnified) { // ── Legacy 路径 ── return this.legacyService.evaluateFeynman(userId, input); } // ═════════════════════════════════════════════════════════ // ── Unified 路径 ── // ═════════════════════════════════════════════════════════ // 3. 确定 knowledgeItemId // 当前请求体不含此字段(契约 U-2),使用传入值或占位符 // M-AI-05-07 及后续客户端升级后可传入真实 ID const resolvedKnowledgeItemId = knowledgeItemId || 'unknown'; // 4. 确定稳定 submissionId(幂等键来源) const submissionId = this.resolveSubmissionId(input); // 5. 构造 idempotencyKey const idempotencyKey = `feynman:${submissionId}`; // 6. 构建 Snapshot const snapshotInput: FeynmanSnapshotInput = { userId, knowledgeItemId: resolvedKnowledgeItemId, knowledgeItemTitle: input.knowledgeItemTitle, knowledgeItemContent: input.knowledgeItemContent, userExplanation: input.userExplanation, submissionId, sessionId: input.sessionId, answerId: input.answerId, }; const snapshot = await this.snapshotBuilder.build(snapshotInput); // 7. 通过 AiJobCreationService 创建 Job(原子:Job + Snapshot + Outbox) const result = await this.creationService.createJob({ userId, jobType: 'feynman_evaluation', triggerType: 'user_api', targetType: 'knowledge_item', targetId: resolvedKnowledgeItemId, idempotencyKey, retrySnapshotContent: snapshot as unknown as Record, }); this.logger.log( `Feynman Unified: jobId=${result.id} userId=${userId} ` + `submissionId=${submissionId} idempotencyKey=${idempotencyKey}`, ); // 8. 返回兼容响应(不删除旧字段,新增可选字段) return { jobId: result.id, status: 'queued', engineMode: 'unified', lifecycleStatus: 'queued', }; } // ── Private Helpers ── /** * 判断是否应使用 Unified 引擎。 * * FeatureFlag 查询失败 → 安全回退到 legacy。 */ private async shouldUseUnified(userId: string): Promise { try { const enabled = await this.featureFlag.isEnabled(FLAG_NAME, userId); this.logger.log( `FEYNMAN_ENGINE_MODE=${enabled ? 'unified' : 'legacy'} for userId=${userId}`, ); return enabled; } catch (err: any) { this.logger.warn( `FeatureFlag query failed, falling back to legacy: ${err.message}`, ); return false; } } /** * 从请求参数解析稳定 submissionId。 * * 优先级: * 1. sessionId + answerId 组合(如都存在) * 2. sessionId(如仅 sessionId 存在) * 3. 基于 content 的 hash 回退(保证相同内容 → 相同 ID) */ private resolveSubmissionId(input: FeynmanEvaluateInput): string { if (input.sessionId && input.answerId) { return `${input.sessionId}:${input.answerId}`; } if (input.sessionId) { return input.sessionId; } // 回退:基于内容 hash 的 submissionId(相同输入 → 相同 key) const contentKey = [ input.knowledgeItemTitle, input.knowledgeItemContent, input.userExplanation, ].join('|'); return crypto.createHash('sha256').update(contentKey).digest('hex').substring(0, 16); } }